Compare commits
101 Commits
Author | SHA1 | Date | |
---|---|---|---|
a69361708e | |||
567d868f76 | |||
12030efee2 | |||
3cbe98d7b8 | |||
503a97bc4c | |||
04c382643d | |||
fc140ad9c1 | |||
ae6a35121b | |||
78a2cc89e9 | |||
dbfe5c8918 | |||
c697ca7ad1 | |||
1b00f4e518 | |||
fb77bf882f | |||
466ebbef3a | |||
458a4c8164 | |||
0363c0854f | |||
558addd097 | |||
233ddc955b | |||
065c711f91 | |||
49655c11c9 | |||
38fd4eda22 | |||
e8f9c57c34 | |||
b71630246a | |||
fd2eac1f08 | |||
1545ff65d3 | |||
ff32c82851 | |||
d531a94594 | |||
71ab9586ac | |||
e1a19be06c | |||
6f2168d641 | |||
cde2121b60 | |||
a0bc37e826 | |||
4d67de8e5f | |||
f983a23b1a | |||
6a1851da13 | |||
ad5381ce34 | |||
dbc7587741 | |||
bc3aa7b8dc | |||
6bf6a9da11 | |||
ab175bdd9a | |||
8dbbea2138 | |||
f6226e6b53 | |||
43d13db07c | |||
82210c37e3 | |||
2816d7217a | |||
2fa868173b | |||
622c75bb42 | |||
2121125283 | |||
c72a117e34 | |||
b5cc32d59f | |||
d943d03266 | |||
6eca8c42f5 | |||
af989ba9f6 | |||
4a65a5b192 | |||
bbbafdfe70 | |||
860095e862 | |||
ff9be43291 | |||
a487378daf | |||
895f5cbb76 | |||
8b9b1460ab | |||
7edd3df074 | |||
16c51f7b07 | |||
7a3a97447f | |||
b500d8e204 | |||
c34a369286 | |||
b9f3ab2e56 | |||
6b59973624 | |||
d18485293d | |||
a82e11d694 | |||
4dc5fc65ac | |||
7463cf6253 | |||
d799ec7ac9 | |||
254719f22f | |||
596e8df4fc | |||
e1e276e1ea | |||
8cdd4311a9 | |||
b7f7b16aef | |||
f13ce6e2b4 | |||
8c10606b61 | |||
7fda4276d6 | |||
7993366bfc | |||
27eb0588d7 | |||
34d34a050a | |||
961bc24f27 | |||
8a90b61b97 | |||
6a8f6f9496 | |||
afb5ae741c | |||
95e41b5570 | |||
eb6fdd900e | |||
88def5eff8 | |||
0e99c81eb8 | |||
38c00ddab5 | |||
c72cc39920 | |||
4ef9fb1f28 | |||
5dd5697f65 | |||
a7c2009e49 | |||
a71ef4a4b2 | |||
30413086fc | |||
98ddf97855 | |||
8f5a210ec7 | |||
ce09b07cfd |
@ -162,7 +162,7 @@ jobs:
|
|||||||
openssl aes-256-cbc -d -in ./app/upload-key-encrypted.jks -k $ENCRYPT_KEY >> ./app/upload-key.jks
|
openssl aes-256-cbc -d -in ./app/upload-key-encrypted.jks -k $ENCRYPT_KEY >> ./app/upload-key.jks
|
||||||
- run:
|
- run:
|
||||||
name: Publish release
|
name: Publish release
|
||||||
command: ./gradlew publishPlayRelease --no-daemon --stacktrace --console=plain -PenableCrashlytics -PdisablePreDex
|
command: ./gradlew publishPlayRelease --no-daemon --stacktrace --console=plain -PdisablePreDex
|
||||||
|
|
||||||
workflows:
|
workflows:
|
||||||
version: 2
|
version: 2
|
||||||
|
2
.github/workflows/deploy-store.yml
vendored
@ -40,7 +40,7 @@ jobs:
|
|||||||
SINGLE_SUPPORT_AD_ID: ${{ secrets.SINGLE_SUPPORT_AD_ID }}
|
SINGLE_SUPPORT_AD_ID: ${{ secrets.SINGLE_SUPPORT_AD_ID }}
|
||||||
DASHBOARD_TILE_AD_ID: ${{ secrets.DASHBOARD_TILE_AD_ID }}
|
DASHBOARD_TILE_AD_ID: ${{ secrets.DASHBOARD_TILE_AD_ID }}
|
||||||
SET_BUILD_TIMESTAMP: ${{ secrets.SET_BUILD_TIMESTAMP }}
|
SET_BUILD_TIMESTAMP: ${{ secrets.SET_BUILD_TIMESTAMP }}
|
||||||
run: ./gradlew publishPlayReleaseApps -PenableFirebase --stacktrace;
|
run: ./gradlew publishPlayReleaseApps --stacktrace;
|
||||||
|
|
||||||
deploy-app-gallery:
|
deploy-app-gallery:
|
||||||
name: AppGallery
|
name: AppGallery
|
||||||
|
5
.github/workflows/deploy-test.yml
vendored
@ -36,8 +36,7 @@ jobs:
|
|||||||
- name: Prepare build configuration
|
- name: Prepare build configuration
|
||||||
run: |
|
run: |
|
||||||
sed -i -e "s#applicationIdSuffix \".dev\"#applicationIdSuffix \".${GITHUB_HEAD_REF//[-.\/]/_}\"#" app/build.gradle
|
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/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
|
sed -i -e '/versionNameSuffix/d' app/build.gradle
|
||||||
- name: Add signing config
|
- name: Add signing config
|
||||||
run: |
|
run: |
|
||||||
@ -131,7 +130,7 @@ jobs:
|
|||||||
BITRISE_KEYSTORE_PASSWORD: ${{ secrets.BITRISE_KEYSTORE_PASSWORD }}
|
BITRISE_KEYSTORE_PASSWORD: ${{ secrets.BITRISE_KEYSTORE_PASSWORD }}
|
||||||
BITRISE_KEY_ALIAS: ${{ secrets.BITRISE_KEY_ALIAS }}
|
BITRISE_KEY_ALIAS: ${{ secrets.BITRISE_KEY_ALIAS }}
|
||||||
BITRISE_KEY_PASSWORD: ${{ secrets.BITRISE_KEY_PASSWORD }}
|
BITRISE_KEY_PASSWORD: ${{ secrets.BITRISE_KEY_PASSWORD }}
|
||||||
run: ./gradlew assemblePlayDebug -PenableFirebase --stacktrace
|
run: ./gradlew assemblePlayDebug --stacktrace
|
||||||
- name: Upload apk to github artifacts
|
- name: Upload apk to github artifacts
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
|
11
.gitignore
vendored
@ -117,12 +117,13 @@ Thumbs.db
|
|||||||
*.ear
|
*.ear
|
||||||
|
|
||||||
### AndroidStudio Patch ###
|
### AndroidStudio Patch ###
|
||||||
|
|
||||||
!/gradle/wrapper/gradle-wrapper.jar
|
!/gradle/wrapper/gradle-wrapper.jar
|
||||||
.idea/jarRepositories.xml
|
.idea/jarRepositories.xml
|
||||||
|
|
||||||
|
### Services config files
|
||||||
|
agconnect-services.json
|
||||||
|
agconnect-credentials.json
|
||||||
|
google-services.json
|
||||||
|
!app/google-services.json
|
||||||
|
|
||||||
|
|
||||||
app/src/release/agconnect-services.json
|
|
||||||
app/src/release/agconnect-credentials.json
|
|
||||||
.idea/deploymentTargetDropDown.xml
|
|
||||||
.idea/kotlinc.xml
|
|
||||||
|
@ -61,7 +61,7 @@ script:
|
|||||||
gpg --yes --batch --passphrase=$SERVICES_ENCRYPT_KEY ./app/src/release/agconnect-services.json.gpg;
|
gpg --yes --batch --passphrase=$SERVICES_ENCRYPT_KEY ./app/src/release/agconnect-services.json.gpg;
|
||||||
gpg --yes --batch --passphrase=$ENCRYPT_KEY ./app/key.p12.gpg;
|
gpg --yes --batch --passphrase=$ENCRYPT_KEY ./app/key.p12.gpg;
|
||||||
gpg --yes --batch --passphrase=$ENCRYPT_KEY ./app/upload-key.jks.gpg;
|
gpg --yes --batch --passphrase=$ENCRYPT_KEY ./app/upload-key.jks.gpg;
|
||||||
./gradlew publishPlayRelease -PenableFirebase --stacktrace;
|
./gradlew publishPlayRelease --stacktrace;
|
||||||
fi
|
fi
|
||||||
|
|
||||||
after_success:
|
after_success:
|
||||||
|
@ -27,15 +27,12 @@ android {
|
|||||||
testApplicationId "io.github.tests.wulkanowy"
|
testApplicationId "io.github.tests.wulkanowy"
|
||||||
minSdkVersion 21
|
minSdkVersion 21
|
||||||
targetSdkVersion 34
|
targetSdkVersion 34
|
||||||
versionCode 152
|
versionCode 164
|
||||||
versionName "2.5.3"
|
versionName "2.6.4"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
resValue "string", "app_name", "Wulkanowy"
|
resValue "string", "app_name", "Wulkanowy"
|
||||||
manifestPlaceholders = [
|
manifestPlaceholders = [admob_project_id: ""]
|
||||||
firebase_enabled: project.hasProperty("enableFirebase"),
|
|
||||||
admob_project_id: ""
|
|
||||||
]
|
|
||||||
|
|
||||||
buildConfigField "String", "SINGLE_SUPPORT_AD_ID", "null"
|
buildConfigField "String", "SINGLE_SUPPORT_AD_ID", "null"
|
||||||
buildConfigField "String", "DASHBOARD_TILE_AD_ID", "null"
|
buildConfigField "String", "DASHBOARD_TILE_AD_ID", "null"
|
||||||
@ -76,7 +73,6 @@ android {
|
|||||||
resValue "string", "app_name", "Wulkanowy DEV"
|
resValue "string", "app_name", "Wulkanowy DEV"
|
||||||
applicationIdSuffix ".dev"
|
applicationIdSuffix ".dev"
|
||||||
versionNameSuffix "-dev"
|
versionNameSuffix "-dev"
|
||||||
ext.enableCrashlytics = project.hasProperty("enableFirebase")
|
|
||||||
buildConfigField "String", "MESSAGES_BASE_URL", "\"https://messages.wulkanowy.net.pl\""
|
buildConfigField "String", "MESSAGES_BASE_URL", "\"https://messages.wulkanowy.net.pl\""
|
||||||
buildConfigField "String", "SCHOOLS_BASE_URL", '"https://schools.wulkanowy.net.pl"'
|
buildConfigField "String", "SCHOOLS_BASE_URL", '"https://schools.wulkanowy.net.pl"'
|
||||||
}
|
}
|
||||||
@ -164,8 +160,8 @@ play {
|
|||||||
defaultToAppBundles = false
|
defaultToAppBundles = false
|
||||||
track = 'production'
|
track = 'production'
|
||||||
releaseStatus = ReleaseStatus.IN_PROGRESS
|
releaseStatus = ReleaseStatus.IN_PROGRESS
|
||||||
userFraction = 0.20d
|
userFraction = 0.99d
|
||||||
updatePriority = 3
|
updatePriority = 4
|
||||||
enabled.set(false)
|
enabled.set(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -195,23 +191,23 @@ ext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation 'io.github.wulkanowy:sdk:2.5.3'
|
implementation 'io.github.wulkanowy:sdk:2.6.3'
|
||||||
|
|
||||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
|
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
|
||||||
|
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3"
|
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3"
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines"
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines"
|
||||||
|
|
||||||
implementation 'androidx.core:core-ktx:1.12.0'
|
implementation 'androidx.core:core-ktx:1.13.1'
|
||||||
implementation 'androidx.core:core-splashscreen:1.0.1'
|
implementation 'androidx.core:core-splashscreen:1.0.1'
|
||||||
implementation "androidx.activity:activity-ktx:1.8.2"
|
implementation "androidx.activity:activity-ktx:1.9.0"
|
||||||
implementation "androidx.appcompat:appcompat:1.6.1"
|
implementation "androidx.appcompat:appcompat:1.6.1"
|
||||||
implementation "androidx.fragment:fragment-ktx:1.6.2"
|
implementation "androidx.fragment:fragment-ktx:1.7.0"
|
||||||
implementation "androidx.annotation:annotation:1.7.1"
|
implementation "androidx.annotation:annotation:1.7.1"
|
||||||
|
|
||||||
implementation "androidx.preference:preference-ktx:1.2.1"
|
implementation "androidx.preference:preference-ktx:1.2.1"
|
||||||
implementation "androidx.recyclerview:recyclerview:1.3.2"
|
implementation "androidx.recyclerview:recyclerview:1.3.2"
|
||||||
implementation "androidx.viewpager2:viewpager2:1.1.0-beta02"
|
implementation "androidx.viewpager2:viewpager2:1.1.0-rc01"
|
||||||
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
|
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
|
||||||
implementation "androidx.constraintlayout:constraintlayout:2.1.4"
|
implementation "androidx.constraintlayout:constraintlayout:2.1.4"
|
||||||
implementation "androidx.coordinatorlayout:coordinatorlayout:1.2.0"
|
implementation "androidx.coordinatorlayout:coordinatorlayout:1.2.0"
|
||||||
@ -237,7 +233,7 @@ dependencies {
|
|||||||
implementation 'com.github.ncapdevi:FragNav:3.3.0'
|
implementation 'com.github.ncapdevi:FragNav:3.3.0'
|
||||||
implementation "com.github.YarikSOffice:lingver:1.3.0"
|
implementation "com.github.YarikSOffice:lingver:1.3.0"
|
||||||
|
|
||||||
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
|
implementation 'com.squareup.retrofit2:retrofit:2.11.0'
|
||||||
implementation "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0"
|
implementation "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0"
|
||||||
implementation "com.squareup.okhttp3:logging-interceptor:4.12.0"
|
implementation "com.squareup.okhttp3:logging-interceptor:4.12.0"
|
||||||
implementation "com.squareup.okhttp3:okhttp-urlconnection:4.12.0"
|
implementation "com.squareup.okhttp3:okhttp-urlconnection:4.12.0"
|
||||||
@ -250,9 +246,9 @@ dependencies {
|
|||||||
implementation "io.github.wulkanowy:AppKillerManager:3.0.1"
|
implementation "io.github.wulkanowy:AppKillerManager:3.0.1"
|
||||||
implementation 'me.xdrop:fuzzywuzzy:1.4.0'
|
implementation 'me.xdrop:fuzzywuzzy:1.4.0'
|
||||||
implementation 'com.fredporciuncula:flow-preferences:1.9.1'
|
implementation 'com.fredporciuncula:flow-preferences:1.9.1'
|
||||||
implementation 'org.apache.commons:commons-text:1.11.0'
|
implementation 'org.apache.commons:commons-text:1.12.0'
|
||||||
|
|
||||||
playImplementation platform('com.google.firebase:firebase-bom:32.7.3')
|
playImplementation platform('com.google.firebase:firebase-bom:33.0.0')
|
||||||
playImplementation 'com.google.firebase:firebase-analytics'
|
playImplementation 'com.google.firebase:firebase-analytics'
|
||||||
playImplementation 'com.google.firebase:firebase-messaging'
|
playImplementation 'com.google.firebase:firebase-messaging'
|
||||||
playImplementation 'com.google.firebase:firebase-crashlytics:'
|
playImplementation 'com.google.firebase:firebase-crashlytics:'
|
||||||
@ -278,7 +274,7 @@ dependencies {
|
|||||||
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines"
|
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines"
|
||||||
testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
|
testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
|
||||||
|
|
||||||
testImplementation 'org.robolectric:robolectric:4.11.1'
|
testImplementation 'org.robolectric:robolectric:4.12.1'
|
||||||
testImplementation "androidx.test:runner:1.5.2"
|
testImplementation "androidx.test:runner:1.5.2"
|
||||||
testImplementation "androidx.test.ext:junit:1.1.5"
|
testImplementation "androidx.test.ext:junit:1.1.5"
|
||||||
testImplementation "androidx.test:core:1.5.0"
|
testImplementation "androidx.test:core:1.5.0"
|
||||||
|
@ -36,6 +36,37 @@
|
|||||||
"status": 2
|
"status": 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"client_info": {
|
||||||
|
"mobilesdk_app_id": "1:1091101852179:android:b558a25f65d088b1",
|
||||||
|
"android_client_info": {
|
||||||
|
"package_name": "io.github.wulkanowy"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"oauth_client": [
|
||||||
|
{
|
||||||
|
"client_id": "",
|
||||||
|
"client_type": 3
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"api_key": [
|
||||||
|
{
|
||||||
|
"current_key": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"services": {
|
||||||
|
"analytics_service": {
|
||||||
|
"status": 1
|
||||||
|
},
|
||||||
|
"appinvite_service": {
|
||||||
|
"status": 1,
|
||||||
|
"other_platform_oauth_client": []
|
||||||
|
},
|
||||||
|
"ads_service": {
|
||||||
|
"status": 2
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"configuration_version": "1"
|
"configuration_version": "1"
|
@ -1,7 +1,8 @@
|
|||||||
#!/bin/bash -
|
#!/bin/bash -
|
||||||
|
|
||||||
content=$(cat < "app/src/main/play/release-notes/pl-PL/default.txt") || exit
|
content=$(cat < "app/src/main/play/release-notes/pl-PL/default.txt") || exit
|
||||||
if [[ "${#content}" -gt 500 ]]; then
|
content2=echo "$content" | dos2unix
|
||||||
|
if [[ "${#content2}" -gt 500 ]]; then
|
||||||
echo >&2 "Release notes content has reached the limit of 500 characters"
|
echo >&2 "Release notes content has reached the limit of 500 characters"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
2559
app/schemas/io.github.wulkanowy.data.db.AppDatabase/64.json
Normal file
@ -1,92 +0,0 @@
|
|||||||
{
|
|
||||||
"agcgw": {
|
|
||||||
"backurl": "connect-dre.hispace.hicloud.com",
|
|
||||||
"url": "connect-dre.dbankcloud.cn",
|
|
||||||
"websocketbackurl": "connect-ws-dre.hispace.dbankcloud.com",
|
|
||||||
"websocketurl": "connect-ws-dre.hispace.dbankcloud.cn"
|
|
||||||
},
|
|
||||||
"agcgw_all": {
|
|
||||||
"CN": "connect-drcn.dbankcloud.cn",
|
|
||||||
"CN_back": "connect-drcn.hispace.hicloud.com",
|
|
||||||
"DE": "connect-dre.dbankcloud.cn",
|
|
||||||
"DE_back": "connect-dre.hispace.hicloud.com",
|
|
||||||
"RU": "connect-drru.hispace.dbankcloud.ru",
|
|
||||||
"RU_back": "connect-drru.hispace.dbankcloud.cn",
|
|
||||||
"SG": "connect-dra.dbankcloud.cn",
|
|
||||||
"SG_back": "connect-dra.hispace.hicloud.com"
|
|
||||||
},
|
|
||||||
"websocketgw_all": {
|
|
||||||
"CN": "connect-ws-drcn.hispace.dbankcloud.cn",
|
|
||||||
"CN_back": "connect-ws-drcn.hispace.dbankcloud.com",
|
|
||||||
"DE": "connect-ws-dre.hispace.dbankcloud.cn",
|
|
||||||
"DE_back": "connect-ws-dre.hispace.dbankcloud.com",
|
|
||||||
"RU": "connect-ws-drru.hispace.dbankcloud.ru",
|
|
||||||
"RU_back": "connect-ws-drru.hispace.dbankcloud.cn",
|
|
||||||
"SG": "connect-ws-dra.hispace.dbankcloud.cn",
|
|
||||||
"SG_back": "connect-ws-dra.hispace.dbankcloud.com"
|
|
||||||
},
|
|
||||||
"client": {
|
|
||||||
"cp_id": "890048000024105546",
|
|
||||||
"product_id": "736430079244736562",
|
|
||||||
"client_id": "514530959291319360",
|
|
||||||
"client_secret": "C42522DBF17D3D4BBE9D9C1783A54484B7E6844B388B7A67502D36A633A4186B",
|
|
||||||
"project_id": "736430079244736562",
|
|
||||||
"app_id": "106552551",
|
|
||||||
"api_key": "CgB6e3x9BUNiq+r8ebCHNojjjYsMT4pJSjjNDOkm9owtBb6rVI6LjnASoZBRxbjjhObcrV5gANo99fI/eKZDTbWS",
|
|
||||||
"package_name": "io.github.wulkanowy.dev"
|
|
||||||
},
|
|
||||||
"oauth_client": {
|
|
||||||
"client_id": "106552551",
|
|
||||||
"client_type": 1
|
|
||||||
},
|
|
||||||
"app_info": {
|
|
||||||
"app_id": "106552551",
|
|
||||||
"package_name": "io.github.wulkanowy.dev"
|
|
||||||
},
|
|
||||||
"service": {
|
|
||||||
"analytics": {
|
|
||||||
"collector_url": "datacollector-dre.dt.hicloud.com,datacollector-dre.dt.dbankcloud.cn",
|
|
||||||
"collector_url_ru": "datacollector-drru.dt.dbankcloud.ru,datacollector-drru.dt.hicloud.com",
|
|
||||||
"collector_url_sg": "datacollector-dra.dt.hicloud.com,datacollector-dra.dt.dbankcloud.cn",
|
|
||||||
"collector_url_de": "datacollector-dre.dt.hicloud.com,datacollector-dre.dt.dbankcloud.cn",
|
|
||||||
"collector_url_cn": "datacollector-drcn.dt.hicloud.com,datacollector-drcn.dt.dbankcloud.cn",
|
|
||||||
"resource_id": "p1",
|
|
||||||
"channel_id": ""
|
|
||||||
},
|
|
||||||
"search":{
|
|
||||||
"url":"https://search-dre.cloud.huawei.com"
|
|
||||||
},
|
|
||||||
"cloudstorage": {
|
|
||||||
"storage_url_sg_back": "https://agc-storage-dra.cloud.huawei.asia",
|
|
||||||
"storage_url_ru_back": "https://agc-storage-drru.cloud.huawei.ru",
|
|
||||||
"storage_url_ru": "https://agc-storage-drru.cloud.huawei.ru",
|
|
||||||
"storage_url_de_back": "https://agc-storage-dre.cloud.huawei.eu",
|
|
||||||
"storage_url_de": "https://ops-dre.agcstorage.link",
|
|
||||||
"storage_url": "https://agc-storage-drcn.platform.dbankcloud.cn",
|
|
||||||
"storage_url_sg": "https://ops-dra.agcstorage.link",
|
|
||||||
"storage_url_cn_back": "https://agc-storage-drcn.cloud.huawei.com.cn",
|
|
||||||
"storage_url_cn": "https://agc-storage-drcn.platform.dbankcloud.cn"
|
|
||||||
},
|
|
||||||
"ml": {
|
|
||||||
"mlservice_url": "ml-api-dre.ai.dbankcloud.com,ml-api-dre.ai.dbankcloud.cn"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"region": "DE",
|
|
||||||
"configuration_version": "3.0",
|
|
||||||
"appInfos": [
|
|
||||||
{
|
|
||||||
"package_name": "io.github.wulkanowy.dev",
|
|
||||||
"client": {
|
|
||||||
"app_id": "106552551"
|
|
||||||
},
|
|
||||||
"app_info": {
|
|
||||||
"package_name": "io.github.wulkanowy.dev",
|
|
||||||
"app_id": "106552551"
|
|
||||||
},
|
|
||||||
"oauth_client": {
|
|
||||||
"client_type": 1,
|
|
||||||
"client_id": "106552551"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
@ -51,7 +51,7 @@
|
|||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="portrait"
|
||||||
android:theme="@style/WulkanowyTheme.SplashScreen"
|
android:theme="@style/WulkanowyTheme.SplashScreen"
|
||||||
tools:ignore="LockedOrientationActivity">
|
tools:ignore="DiscouragedApi,LockedOrientationActivity">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
@ -155,33 +155,9 @@
|
|||||||
android:resource="@xml/provider_paths" />
|
android:resource="@xml/provider_paths" />
|
||||||
</provider>
|
</provider>
|
||||||
|
|
||||||
<!-- workaround for https://github.com/firebase/firebase-android-sdk/issues/473 enabled:false -->
|
|
||||||
<!-- https://firebase.googleblog.com/2017/03/take-control-of-your-firebase-init-on.html -->
|
|
||||||
<provider
|
|
||||||
android:name="com.google.firebase.provider.FirebaseInitProvider"
|
|
||||||
android:authorities="${applicationId}.firebaseinitprovider"
|
|
||||||
android:enabled="${firebase_enabled}"
|
|
||||||
android:exported="false"
|
|
||||||
tools:ignore="MissingClass" />
|
|
||||||
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="install_channel"
|
android:name="install_channel"
|
||||||
android:value="${install_channel}" />
|
android:value="${install_channel}" />
|
||||||
<meta-data
|
|
||||||
android:name="firebase_analytics_collection_enabled"
|
|
||||||
android:value="${firebase_enabled}" />
|
|
||||||
<meta-data
|
|
||||||
android:name="google_analytics_adid_collection_enabled"
|
|
||||||
android:value="${firebase_enabled}" />
|
|
||||||
<meta-data
|
|
||||||
android:name="firebase_crashlytics_collection_enabled"
|
|
||||||
android:value="${firebase_enabled}" />
|
|
||||||
<meta-data
|
|
||||||
android:name="firebase_messaging_auto_init_enabled"
|
|
||||||
android:value="${firebase_enabled}" />
|
|
||||||
<meta-data
|
|
||||||
android:name="firebase_inapp_messaging_auto_data_collection_enabled"
|
|
||||||
android:value="${firebase_enabled}" />
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="com.google.firebase.messaging.default_notification_icon"
|
android:name="com.google.firebase.messaging.default_notification_icon"
|
||||||
android:resource="@drawable/ic_stat_all" />
|
android:resource="@drawable/ic_stat_all" />
|
||||||
|
@ -1,11 +1,17 @@
|
|||||||
package io.github.wulkanowy.data
|
package io.github.wulkanowy.data
|
||||||
|
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.FlowPreview
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.catch
|
import kotlinx.coroutines.flow.catch
|
||||||
import kotlinx.coroutines.flow.collect
|
import kotlinx.coroutines.flow.collect
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.debounce
|
||||||
import kotlinx.coroutines.flow.emitAll
|
import kotlinx.coroutines.flow.emitAll
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
|
import kotlinx.coroutines.flow.filterNot
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.flow.flowOf
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
@ -14,16 +20,39 @@ import kotlinx.coroutines.flow.takeWhile
|
|||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import kotlin.time.Duration
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
sealed class Resource<T> {
|
sealed interface Resource<out T> {
|
||||||
|
/**
|
||||||
open class Loading<T> : Resource<T>()
|
* The initial value of a resource flow. Indicates no data that is currently available to be shown,
|
||||||
|
* however with the expectation that the state will transition to another one soon.
|
||||||
|
*/
|
||||||
|
open class Loading<T> : Resource<T>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A semi-loading state with some data available to be displayed (usually cached data loaded from
|
||||||
|
* the database). Still not the target state and it's expected to transition into another one soon.
|
||||||
|
*/
|
||||||
data class Intermediate<T>(val data: T) : Loading<T>()
|
data class Intermediate<T>(val data: T) : Loading<T>()
|
||||||
|
|
||||||
data class Success<T>(val data: T) : Resource<T>()
|
/**
|
||||||
|
* The happy-path target state. Data can either be:
|
||||||
|
* - loaded from the database - while it may seem like this case is already handled by the
|
||||||
|
* Intermediate state, the difference here is semantic. Cached data is returned as Intermediate
|
||||||
|
* when there's a API request in progress (or soon expected to be), however when there is no
|
||||||
|
* intention of immediately querying the API, the cached data is returned as a Success.
|
||||||
|
* - fetched from the API.
|
||||||
|
*/
|
||||||
|
data class Success<T>(val data: T) : Resource<T>
|
||||||
|
|
||||||
data class Error<T>(val error: Throwable) : Resource<T>()
|
/**
|
||||||
|
* Something bad happened and we were unable to get the requested data. This can be caused by
|
||||||
|
* a database error, a network error, or really just any other error. Upon receiving this state
|
||||||
|
* the UI can either: display a full screen error, or, when it has received any data previously,
|
||||||
|
* display a snack bar informing of the problem.
|
||||||
|
*/
|
||||||
|
data class Error<T>(val error: Throwable) : Resource<T>
|
||||||
}
|
}
|
||||||
|
|
||||||
val <T> Resource<T>.dataOrNull: T?
|
val <T> Resource<T>.dataOrNull: T?
|
||||||
@ -64,6 +93,22 @@ fun <T, U> Resource<T>.mapData(block: (T) -> U) = when (this) {
|
|||||||
is Resource.Error -> Resource.Error(this.error)
|
is Resource.Error -> Resource.Error(this.error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injects another flow into this flow's resource data.
|
||||||
|
*/
|
||||||
|
inline fun <T1, T2, R> Flow<Resource<T1>>.combineWithResourceData(
|
||||||
|
flow: Flow<T2>,
|
||||||
|
crossinline block: suspend (T1, T2) -> R
|
||||||
|
): Flow<Resource<R>> =
|
||||||
|
combine(flow) { resource, inject ->
|
||||||
|
when (resource) {
|
||||||
|
is Resource.Success -> Resource.Success(block(resource.data, inject))
|
||||||
|
is Resource.Intermediate -> Resource.Intermediate(block(resource.data, inject))
|
||||||
|
is Resource.Loading -> Resource.Loading()
|
||||||
|
is Resource.Error -> Resource.Error(resource.error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun <T> Flow<Resource<T>>.logResourceStatus(name: String, showData: Boolean = false) = onEach {
|
fun <T> Flow<Resource<T>>.logResourceStatus(name: String, showData: Boolean = false) = onEach {
|
||||||
val description = when (it) {
|
val description = when (it) {
|
||||||
is Resource.Intermediate -> "intermediate data received" + if (showData) " (data: `${it.data}`)" else ""
|
is Resource.Intermediate -> "intermediate data received" + if (showData) " (data: `${it.data}`)" else ""
|
||||||
@ -74,8 +119,29 @@ fun <T> Flow<Resource<T>>.logResourceStatus(name: String, showData: Boolean = fa
|
|||||||
Timber.i("$name: $description")
|
Timber.i("$name: $description")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <T, U> Flow<Resource<T>>.mapResourceData(block: (T) -> U) = map {
|
inline fun <T, U> Flow<Resource<T>>.mapResourceData(crossinline block: suspend (T) -> U) = map {
|
||||||
it.mapData(block)
|
when (it) {
|
||||||
|
is Resource.Success -> Resource.Success(block(it.data))
|
||||||
|
is Resource.Intermediate -> Resource.Intermediate(block(it.data))
|
||||||
|
is Resource.Loading -> Resource.Loading()
|
||||||
|
is Resource.Error -> Resource.Error(it.error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
fun <T, U> Flow<Resource<T>>.flatMapResourceData(
|
||||||
|
inheritIntermediate: Boolean = true, block: suspend (T) -> Flow<Resource<U>>
|
||||||
|
) = flatMapLatest {
|
||||||
|
when (it) {
|
||||||
|
is Resource.Success -> block(it.data)
|
||||||
|
is Resource.Intermediate -> block(it.data).map { newRes ->
|
||||||
|
if (inheritIntermediate && newRes is Resource.Success) Resource.Intermediate(newRes.data)
|
||||||
|
else newRes
|
||||||
|
}
|
||||||
|
|
||||||
|
is Resource.Loading -> flowOf(Resource.Loading())
|
||||||
|
is Resource.Error -> flowOf(Resource.Error(it.error))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <T> Flow<Resource<T>>.onResourceData(block: suspend (T) -> Unit) = onEach {
|
fun <T> Flow<Resource<T>>.onResourceData(block: suspend (T) -> Unit) = onEach {
|
||||||
@ -105,13 +171,13 @@ fun <T> Flow<Resource<T>>.onResourceSuccess(block: suspend (T) -> Unit) = onEach
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <T> Flow<Resource<T>>.onResourceError(block: (Throwable) -> Unit) = onEach {
|
fun <T> Flow<Resource<T>>.onResourceError(block: suspend (Throwable) -> Unit) = onEach {
|
||||||
if (it is Resource.Error) {
|
if (it is Resource.Error) {
|
||||||
block(it.error)
|
block(it.error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <T> Flow<Resource<T>>.onResourceNotLoading(block: () -> Unit) = onEach {
|
fun <T> Flow<Resource<T>>.onResourceNotLoading(block: suspend () -> Unit) = onEach {
|
||||||
if (it !is Resource.Loading) {
|
if (it !is Resource.Loading) {
|
||||||
block()
|
block()
|
||||||
}
|
}
|
||||||
@ -121,70 +187,99 @@ suspend fun <T> Flow<Resource<T>>.toFirstResult() = filter { it !is Resource.Loa
|
|||||||
|
|
||||||
suspend fun <T> Flow<Resource<T>>.waitForResult() = takeWhile { it is Resource.Loading }.collect()
|
suspend fun <T> Flow<Resource<T>>.waitForResult() = takeWhile { it is Resource.Loading }.collect()
|
||||||
|
|
||||||
inline fun <ResultType, RequestType> networkBoundResource(
|
// Can cause excessive amounts of `Resource.Intermediate` to be emitted. Unless that is desired,
|
||||||
mutex: Mutex = Mutex(),
|
// use `debounceIntermediates` to alleviate this behavior.
|
||||||
showSavedOnLoading: Boolean = true,
|
inline fun <reified T> combineResourceFlows(flows: Iterable<Flow<Resource<T>>>): Flow<Resource<List<T>>> =
|
||||||
crossinline isResultEmpty: (ResultType) -> Boolean,
|
combine(flows) { items ->
|
||||||
crossinline query: () -> Flow<ResultType>,
|
var isIntermediate = false
|
||||||
crossinline fetch: suspend (ResultType) -> RequestType,
|
val data = mutableListOf<T>()
|
||||||
crossinline saveFetchResult: suspend (old: ResultType, new: RequestType) -> Unit,
|
for (item in items) {
|
||||||
crossinline onFetchFailed: (Throwable) -> Unit = { },
|
when (item) {
|
||||||
crossinline shouldFetch: (ResultType) -> Boolean = { true },
|
is Resource.Success -> data.add(item.data)
|
||||||
crossinline filterResult: (ResultType) -> ResultType = { it }
|
is Resource.Intermediate -> {
|
||||||
) = flow {
|
isIntermediate = true
|
||||||
emit(Resource.Loading())
|
data.add(item.data)
|
||||||
|
}
|
||||||
|
|
||||||
val data = query().first()
|
is Resource.Loading -> return@combine Resource.Loading()
|
||||||
emitAll(if (shouldFetch(data)) {
|
is Resource.Error -> continue
|
||||||
val filteredResult = filterResult(data)
|
}
|
||||||
|
|
||||||
if (showSavedOnLoading && !isResultEmpty(filteredResult)) {
|
|
||||||
emit(Resource.Intermediate(filteredResult))
|
|
||||||
}
|
}
|
||||||
|
if (data.isEmpty()) {
|
||||||
try {
|
// All items have to be errors for this to happen, so just return the first one.
|
||||||
val newData = fetch(data)
|
// mapData is functionally useless and exists only to satisfy the type checker
|
||||||
mutex.withLock { saveFetchResult(query().first(), newData) }
|
items.first().mapData { listOf(it) }
|
||||||
query().map { Resource.Success(filterResult(it)) }
|
} else if (isIntermediate) {
|
||||||
} catch (throwable: Throwable) {
|
Resource.Intermediate(data)
|
||||||
onFetchFailed(throwable)
|
} else {
|
||||||
flowOf(Resource.Error(throwable))
|
Resource.Success(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(FlowPreview::class)
|
||||||
|
fun <T> Flow<Resource<T>>.debounceIntermediates(timeout: Duration = 5.seconds) = flow {
|
||||||
|
var wasIntermediate = false
|
||||||
|
|
||||||
|
emitAll(this@debounceIntermediates.debounce {
|
||||||
|
if (it is Resource.Intermediate) {
|
||||||
|
if (!wasIntermediate) {
|
||||||
|
wasIntermediate = true
|
||||||
|
Duration.ZERO
|
||||||
|
} else {
|
||||||
|
timeout
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
wasIntermediate = false
|
||||||
|
Duration.ZERO
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
query().map { Resource.Success(filterResult(it)) }
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
inline fun <OutputType, ApiType> networkBoundResource(
|
||||||
|
mutex: Mutex = Mutex(),
|
||||||
|
crossinline isResultEmpty: (OutputType) -> Boolean,
|
||||||
|
crossinline query: () -> Flow<OutputType>,
|
||||||
|
crossinline fetch: suspend () -> ApiType,
|
||||||
|
crossinline saveFetchResult: suspend (old: OutputType, new: ApiType) -> Unit,
|
||||||
|
crossinline shouldFetch: (OutputType) -> Boolean = { true },
|
||||||
|
crossinline filterResult: (OutputType) -> OutputType = { it }
|
||||||
|
) = networkBoundResource(
|
||||||
|
mutex = mutex,
|
||||||
|
isResultEmpty = isResultEmpty,
|
||||||
|
query = query,
|
||||||
|
fetch = fetch,
|
||||||
|
saveFetchResult = saveFetchResult,
|
||||||
|
shouldFetch = shouldFetch,
|
||||||
|
mapResult = filterResult
|
||||||
|
)
|
||||||
|
|
||||||
@JvmName("networkBoundResourceWithMap")
|
@JvmName("networkBoundResourceWithMap")
|
||||||
inline fun <ResultType, RequestType, T> networkBoundResource(
|
inline fun <DatabaseType, ApiType, OutputType> networkBoundResource(
|
||||||
mutex: Mutex = Mutex(),
|
mutex: Mutex = Mutex(),
|
||||||
showSavedOnLoading: Boolean = true,
|
crossinline isResultEmpty: (OutputType) -> Boolean,
|
||||||
crossinline isResultEmpty: (T) -> Boolean,
|
crossinline query: () -> Flow<DatabaseType>,
|
||||||
crossinline query: () -> Flow<ResultType>,
|
crossinline fetch: suspend () -> ApiType,
|
||||||
crossinline fetch: suspend (ResultType) -> RequestType,
|
crossinline saveFetchResult: suspend (old: DatabaseType, new: ApiType) -> Unit,
|
||||||
crossinline saveFetchResult: suspend (old: ResultType, new: RequestType) -> Unit,
|
crossinline shouldFetch: (DatabaseType) -> Boolean = { true },
|
||||||
crossinline onFetchFailed: (Throwable) -> Unit = { },
|
crossinline mapResult: (DatabaseType) -> OutputType,
|
||||||
crossinline shouldFetch: (ResultType) -> Boolean = { true },
|
|
||||||
crossinline mapResult: (ResultType) -> T,
|
|
||||||
) = flow {
|
) = flow {
|
||||||
emit(Resource.Loading())
|
emit(Resource.Loading())
|
||||||
|
|
||||||
val data = query().first()
|
val data = query().first()
|
||||||
emitAll(if (shouldFetch(data)) {
|
if (shouldFetch(data)) {
|
||||||
val mappedResult = mapResult(data)
|
emit(Resource.Intermediate(data))
|
||||||
|
|
||||||
if (showSavedOnLoading && !isResultEmpty(mappedResult)) {
|
|
||||||
emit(Resource.Intermediate(mappedResult))
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
val newData = fetch(data)
|
val newData = fetch()
|
||||||
mutex.withLock { saveFetchResult(query().first(), newData) }
|
mutex.withLock { saveFetchResult(query().first(), newData) }
|
||||||
query().map { Resource.Success(mapResult(it)) }
|
|
||||||
} catch (throwable: Throwable) {
|
} catch (throwable: Throwable) {
|
||||||
onFetchFailed(throwable)
|
emit(Resource.Error(throwable))
|
||||||
flowOf(Resource.Error(throwable))
|
return@flow
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
query().map { Resource.Success(mapResult(it)) }
|
|
||||||
})
|
emitAll(query().map { Resource.Success(it) })
|
||||||
}
|
}
|
||||||
|
.mapResourceData { mapResult(it) }
|
||||||
|
.filterNot { it is Resource.Intermediate && isResultEmpty(it.data) }
|
||||||
|
@ -23,6 +23,7 @@ class WulkanowySdkFactory @Inject constructor(
|
|||||||
) {
|
) {
|
||||||
|
|
||||||
private val eduOneMutex = Mutex()
|
private val eduOneMutex = Mutex()
|
||||||
|
private val migrationFailedStudentIds = mutableSetOf<Long>()
|
||||||
|
|
||||||
private val sdk = Sdk().apply {
|
private val sdk = Sdk().apply {
|
||||||
androidVersion = android.os.Build.VERSION.RELEASE
|
androidVersion = android.os.Build.VERSION.RELEASE
|
||||||
@ -78,14 +79,24 @@ class WulkanowySdkFactory @Inject constructor(
|
|||||||
private suspend fun checkEduOneAndMigrateIfNecessary(student: Student): Boolean {
|
private suspend fun checkEduOneAndMigrateIfNecessary(student: Student): Boolean {
|
||||||
if (student.isEduOne != null) return student.isEduOne
|
if (student.isEduOne != null) return student.isEduOne
|
||||||
|
|
||||||
|
if (student.id in migrationFailedStudentIds) {
|
||||||
|
Timber.i("Migration eduOne: skipping because of previous failure")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
eduOneMutex.withLock {
|
eduOneMutex.withLock {
|
||||||
|
if (student.id in migrationFailedStudentIds) {
|
||||||
|
Timber.i("Migration eduOne: skipping because of previous failure")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
val studentFromDatabase = studentDb.loadById(student.id)
|
val studentFromDatabase = studentDb.loadById(student.id)
|
||||||
if (studentFromDatabase?.isEduOne != null) {
|
if (studentFromDatabase?.isEduOne != null) {
|
||||||
Timber.d("Migration eduOne: already done")
|
Timber.i("Migration eduOne: already done")
|
||||||
return studentFromDatabase.isEduOne
|
return studentFromDatabase.isEduOne
|
||||||
}
|
}
|
||||||
|
|
||||||
Timber.d("Migration eduOne: flag missing. Running migration...")
|
Timber.i("Migration eduOne: flag missing. Running migration...")
|
||||||
val initializedSdk = buildSdk(
|
val initializedSdk = buildSdk(
|
||||||
student = student,
|
student = student,
|
||||||
semester = null,
|
semester = null,
|
||||||
@ -96,11 +107,12 @@ class WulkanowySdkFactory @Inject constructor(
|
|||||||
.getOrNull()
|
.getOrNull()
|
||||||
|
|
||||||
if (newCurrentStudent == null) {
|
if (newCurrentStudent == null) {
|
||||||
Timber.d("Migration eduOne: failed, so skipping")
|
Timber.i("Migration eduOne: failed, so skipping")
|
||||||
|
migrationFailedStudentIds.add(student.id)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
Timber.d("Migration eduOne: success. New isEduOne flag: ${newCurrentStudent.isEduOne}")
|
Timber.i("Migration eduOne: success. New isEduOne flag: ${newCurrentStudent.isEduOne}")
|
||||||
|
|
||||||
val studentIsEduOne = StudentIsEduOne(
|
val studentIsEduOne = StudentIsEduOne(
|
||||||
id = student.id,
|
id = student.id,
|
||||||
|
@ -177,6 +177,7 @@ import javax.inject.Singleton
|
|||||||
AutoMigration(from = 60, to = 61),
|
AutoMigration(from = 60, to = 61),
|
||||||
AutoMigration(from = 61, to = 62),
|
AutoMigration(from = 61, to = 62),
|
||||||
AutoMigration(from = 62, to = 63, spec = Migration63::class),
|
AutoMigration(from = 62, to = 63, spec = Migration63::class),
|
||||||
|
AutoMigration(from = 63, to = 64),
|
||||||
],
|
],
|
||||||
version = AppDatabase.VERSION_SCHEMA,
|
version = AppDatabase.VERSION_SCHEMA,
|
||||||
exportSchema = true
|
exportSchema = true
|
||||||
@ -185,7 +186,7 @@ import javax.inject.Singleton
|
|||||||
abstract class AppDatabase : RoomDatabase() {
|
abstract class AppDatabase : RoomDatabase() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val VERSION_SCHEMA = 63
|
const val VERSION_SCHEMA = 64
|
||||||
|
|
||||||
fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf(
|
fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf(
|
||||||
Migration2(),
|
Migration2(),
|
||||||
|
@ -8,6 +8,6 @@ import kotlinx.coroutines.flow.Flow
|
|||||||
@Dao
|
@Dao
|
||||||
interface MobileDeviceDao : BaseDao<MobileDevice> {
|
interface MobileDeviceDao : BaseDao<MobileDevice> {
|
||||||
|
|
||||||
@Query("SELECT * FROM MobileDevices WHERE user_login_id = :userLoginId ORDER BY date DESC")
|
@Query("SELECT * FROM MobileDevices WHERE user_login_id = :studentId ORDER BY date DESC")
|
||||||
fun loadAll(userLoginId: Int): Flow<List<MobileDevice>>
|
fun loadAll(studentId: Int): Flow<List<MobileDevice>>
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,6 @@ import javax.inject.Singleton
|
|||||||
@Singleton
|
@Singleton
|
||||||
interface SchoolAnnouncementDao : BaseDao<SchoolAnnouncement> {
|
interface SchoolAnnouncementDao : BaseDao<SchoolAnnouncement> {
|
||||||
|
|
||||||
@Query("SELECT * FROM SchoolAnnouncements WHERE user_login_id = :userLoginId ORDER BY date DESC")
|
@Query("SELECT * FROM SchoolAnnouncements WHERE user_login_id = :studentId ORDER BY date DESC")
|
||||||
fun loadAll(userLoginId: Int): Flow<List<SchoolAnnouncement>>
|
fun loadAll(studentId: Int): Flow<List<SchoolAnnouncement>>
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,6 @@ interface SemesterDao : BaseDao<Semester> {
|
|||||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||||
suspend fun insertSemesters(items: List<Semester>): List<Long>
|
suspend fun insertSemesters(items: List<Semester>): List<Long>
|
||||||
|
|
||||||
@Query("SELECT * FROM Semesters WHERE student_id = :studentId AND class_id = :classId")
|
@Query("SELECT * FROM Semesters WHERE (student_id = :studentId AND class_id = :classId) OR (student_id = :studentId AND class_id = 0)")
|
||||||
suspend fun loadAll(studentId: Int, classId: Int): List<Semester>
|
suspend fun loadAll(studentId: Int, classId: Int): List<Semester>
|
||||||
}
|
}
|
||||||
|
@ -47,11 +47,11 @@ abstract class StudentDao {
|
|||||||
abstract suspend fun loadAll(): List<Student>
|
abstract suspend fun loadAll(): List<Student>
|
||||||
|
|
||||||
@Transaction
|
@Transaction
|
||||||
@Query("SELECT * FROM Students JOIN Semesters ON Students.student_id = Semesters.student_id AND Students.class_id = Semesters.class_id")
|
@Query("SELECT * FROM Students JOIN Semesters ON (Students.student_id = Semesters.student_id AND Students.class_id = Semesters.class_id) OR (Students.student_id = Semesters.student_id AND Semesters.class_id = 0)")
|
||||||
abstract suspend fun loadStudentsWithSemesters(): Map<Student, List<Semester>>
|
abstract suspend fun loadStudentsWithSemesters(): Map<Student, List<Semester>>
|
||||||
|
|
||||||
@Transaction
|
@Transaction
|
||||||
@Query("SELECT * FROM Students JOIN Semesters ON Students.student_id = Semesters.student_id AND Students.class_id = Semesters.class_id WHERE Students.id = :id")
|
@Query("SELECT * FROM Students JOIN Semesters ON (Students.student_id = Semesters.student_id AND Students.class_id = Semesters.class_id) OR (Students.student_id = Semesters.student_id AND Semesters.class_id = 0) WHERE Students.id = :id")
|
||||||
abstract suspend fun loadStudentWithSemestersById(id: Long): Map<Student, List<Semester>>
|
abstract suspend fun loadStudentWithSemestersById(id: Long): Map<Student, List<Semester>>
|
||||||
|
|
||||||
@Query("UPDATE Students SET is_current = 1 WHERE id = :id")
|
@Query("UPDATE Students SET is_current = 1 WHERE id = :id")
|
||||||
|
@ -4,6 +4,8 @@ import androidx.room.ColumnInfo
|
|||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
import io.github.wulkanowy.data.enums.MessageType
|
import io.github.wulkanowy.data.enums.MessageType
|
||||||
|
import io.github.wulkanowy.data.serializers.SafeMessageTypeEnumListSerializer
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@ -34,6 +36,8 @@ data class AdminMessage(
|
|||||||
|
|
||||||
val priority: String,
|
val priority: String,
|
||||||
|
|
||||||
|
@SerialName("messageTypes")
|
||||||
|
@Serializable(with = SafeMessageTypeEnumListSerializer::class)
|
||||||
@ColumnInfo(name = "types", defaultValue = "[]")
|
@ColumnInfo(name = "types", defaultValue = "[]")
|
||||||
val types: List<MessageType> = emptyList(),
|
val types: List<MessageType> = emptyList(),
|
||||||
|
|
||||||
|
@ -33,7 +33,13 @@ data class GradeSummary(
|
|||||||
@ColumnInfo(name = "points_sum")
|
@ColumnInfo(name = "points_sum")
|
||||||
val pointsSum: String,
|
val pointsSum: String,
|
||||||
|
|
||||||
val average: Double
|
@ColumnInfo(name = "points_sum_all_year")
|
||||||
|
val pointsSumAllYear: String?,
|
||||||
|
|
||||||
|
val average: Double,
|
||||||
|
|
||||||
|
@ColumnInfo(name = "average_all_year")
|
||||||
|
val averageAllYear: Double? = null,
|
||||||
) {
|
) {
|
||||||
@PrimaryKey(autoGenerate = true)
|
@PrimaryKey(autoGenerate = true)
|
||||||
var id: Long = 0
|
var id: Long = 0
|
||||||
|
@ -9,8 +9,8 @@ import java.time.Instant
|
|||||||
@Entity(tableName = "MobileDevices")
|
@Entity(tableName = "MobileDevices")
|
||||||
data class MobileDevice(
|
data class MobileDevice(
|
||||||
|
|
||||||
@ColumnInfo(name = "user_login_id")
|
@ColumnInfo(name = "user_login_id") // todo: change column name
|
||||||
val userLoginId: Int,
|
val studentId: Int,
|
||||||
|
|
||||||
@ColumnInfo(name = "device_id")
|
@ColumnInfo(name = "device_id")
|
||||||
val deviceId: Int,
|
val deviceId: Int,
|
||||||
|
@ -9,8 +9,8 @@ import java.time.LocalDate
|
|||||||
@Entity(tableName = "SchoolAnnouncements")
|
@Entity(tableName = "SchoolAnnouncements")
|
||||||
data class SchoolAnnouncement(
|
data class SchoolAnnouncement(
|
||||||
|
|
||||||
@ColumnInfo(name = "user_login_id")
|
@ColumnInfo(name = "user_login_id") // todo: change column name
|
||||||
val userLoginId: Int,
|
val studentId: Int,
|
||||||
|
|
||||||
val date: LocalDate,
|
val date: LocalDate,
|
||||||
|
|
||||||
|
@ -49,6 +49,7 @@ data class Student(
|
|||||||
@ColumnInfo(name = "student_id")
|
@ColumnInfo(name = "student_id")
|
||||||
val studentId: Int,
|
val studentId: Int,
|
||||||
|
|
||||||
|
@Deprecated("not available in VULCAN anymore")
|
||||||
@ColumnInfo(name = "user_login_id")
|
@ColumnInfo(name = "user_login_id")
|
||||||
val userLoginId: Int,
|
val userLoginId: Int,
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -4,6 +4,8 @@ enum class MessageType {
|
|||||||
GENERAL_MESSAGE,
|
GENERAL_MESSAGE,
|
||||||
DASHBOARD_MESSAGE,
|
DASHBOARD_MESSAGE,
|
||||||
LOGIN_MESSAGE,
|
LOGIN_MESSAGE,
|
||||||
|
LOGIN_STUDENT_SELECT_MESSAGE,
|
||||||
|
LOGIN_SYMBOL_MESSAGE,
|
||||||
PASS_RESET_MESSAGE,
|
PASS_RESET_MESSAGE,
|
||||||
ERROR_OVERRIDE,
|
ERROR_OVERRIDE,
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -8,7 +8,7 @@ import io.github.wulkanowy.sdk.pojo.LastAnnouncement as SdkLastAnnouncement
|
|||||||
@JvmName("mapDirectorInformationToEntities")
|
@JvmName("mapDirectorInformationToEntities")
|
||||||
fun List<SdkDirectorInformation>.mapToEntities(student: Student) = map {
|
fun List<SdkDirectorInformation>.mapToEntities(student: Student) = map {
|
||||||
SchoolAnnouncement(
|
SchoolAnnouncement(
|
||||||
userLoginId = student.userLoginId,
|
studentId = student.studentId,
|
||||||
date = it.date,
|
date = it.date,
|
||||||
subject = it.subject,
|
subject = it.subject,
|
||||||
content = it.content,
|
content = it.content,
|
||||||
@ -19,7 +19,7 @@ fun List<SdkDirectorInformation>.mapToEntities(student: Student) = map {
|
|||||||
@JvmName("mapLastAnnouncementsToEntities")
|
@JvmName("mapLastAnnouncementsToEntities")
|
||||||
fun List<SdkLastAnnouncement>.mapToEntities(student: Student) = map {
|
fun List<SdkLastAnnouncement>.mapToEntities(student: Student) = map {
|
||||||
SchoolAnnouncement(
|
SchoolAnnouncement(
|
||||||
userLoginId = student.userLoginId,
|
studentId = student.studentId,
|
||||||
date = it.date,
|
date = it.date,
|
||||||
subject = it.subject,
|
subject = it.subject,
|
||||||
content = it.content,
|
content = it.content,
|
||||||
|
@ -37,9 +37,11 @@ fun List<SdkGradeSummary>.mapToEntities(semester: Semester) = map {
|
|||||||
predictedGrade = it.predicted,
|
predictedGrade = it.predicted,
|
||||||
finalGrade = it.final,
|
finalGrade = it.final,
|
||||||
pointsSum = it.pointsSum,
|
pointsSum = it.pointsSum,
|
||||||
|
pointsSumAllYear = it.pointsSumAllYear,
|
||||||
proposedPoints = it.proposedPoints,
|
proposedPoints = it.proposedPoints,
|
||||||
finalPoints = it.finalPoints,
|
finalPoints = it.finalPoints,
|
||||||
average = it.average
|
average = it.average,
|
||||||
|
averageAllYear = it.averageAllYear,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ import io.github.wulkanowy.sdk.pojo.Token as SdkToken
|
|||||||
|
|
||||||
fun List<SdkDevice>.mapToEntities(student: Student) = map {
|
fun List<SdkDevice>.mapToEntities(student: Student) = map {
|
||||||
MobileDevice(
|
MobileDevice(
|
||||||
userLoginId = student.userLoginId,
|
studentId = student.studentId,
|
||||||
date = it.createDate.toInstant(),
|
date = it.createDate.toInstant(),
|
||||||
deviceId = it.id,
|
deviceId = it.id,
|
||||||
name = it.name
|
name = it.name
|
||||||
|
@ -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
|
||||||
|
}
|
@ -6,6 +6,7 @@ import io.github.wulkanowy.data.db.dao.AdminMessageDao
|
|||||||
import io.github.wulkanowy.data.db.entities.AdminMessage
|
import io.github.wulkanowy.data.db.entities.AdminMessage
|
||||||
import io.github.wulkanowy.data.networkBoundResource
|
import io.github.wulkanowy.data.networkBoundResource
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.filterNot
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
@ -28,6 +29,6 @@ class AdminMessageRepository @Inject constructor(
|
|||||||
saveFetchResult = { oldItems, newItems ->
|
saveFetchResult = { oldItems, newItems ->
|
||||||
adminMessageDao.removeOldAndSaveNew(oldItems, newItems)
|
adminMessageDao.removeOldAndSaveNew(oldItems, newItems)
|
||||||
},
|
},
|
||||||
showSavedOnLoading = false,
|
|
||||||
)
|
)
|
||||||
|
.filterNot { it is Resource.Intermediate }
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,8 @@ import io.github.wulkanowy.data.db.entities.LuckyNumber
|
|||||||
import io.github.wulkanowy.data.db.entities.Student
|
import io.github.wulkanowy.data.db.entities.Student
|
||||||
import io.github.wulkanowy.data.mappers.mapToEntity
|
import io.github.wulkanowy.data.mappers.mapToEntity
|
||||||
import io.github.wulkanowy.data.networkBoundResource
|
import io.github.wulkanowy.data.networkBoundResource
|
||||||
|
import io.github.wulkanowy.ui.modules.luckynumberwidget.LuckyNumberWidgetProvider
|
||||||
|
import io.github.wulkanowy.utils.AppWidgetUpdater
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
@ -18,6 +20,7 @@ import javax.inject.Singleton
|
|||||||
class LuckyNumberRepository @Inject constructor(
|
class LuckyNumberRepository @Inject constructor(
|
||||||
private val luckyNumberDb: LuckyNumberDao,
|
private val luckyNumberDb: LuckyNumberDao,
|
||||||
private val wulkanowySdkFactory: WulkanowySdkFactory,
|
private val wulkanowySdkFactory: WulkanowySdkFactory,
|
||||||
|
private val appWidgetUpdater: AppWidgetUpdater,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val saveFetchResultMutex = Mutex()
|
private val saveFetchResultMutex = Mutex()
|
||||||
@ -26,6 +29,7 @@ class LuckyNumberRepository @Inject constructor(
|
|||||||
student: Student,
|
student: Student,
|
||||||
forceRefresh: Boolean,
|
forceRefresh: Boolean,
|
||||||
notify: Boolean = false,
|
notify: Boolean = false,
|
||||||
|
isFromAppWidget: Boolean = false
|
||||||
) = networkBoundResource(
|
) = networkBoundResource(
|
||||||
mutex = saveFetchResultMutex,
|
mutex = saveFetchResultMutex,
|
||||||
isResultEmpty = { it == null },
|
isResultEmpty = { it == null },
|
||||||
@ -44,6 +48,9 @@ class LuckyNumberRepository @Inject constructor(
|
|||||||
oldItems = listOfNotNull(oldLuckyNumber),
|
oldItems = listOfNotNull(oldLuckyNumber),
|
||||||
newItems = listOf(newLuckyNumber.apply { if (notify) isNotified = false }),
|
newItems = listOf(newLuckyNumber.apply { if (notify) isNotified = false }),
|
||||||
)
|
)
|
||||||
|
if (!isFromAppWidget) {
|
||||||
|
appWidgetUpdater.updateAllAppWidgetsByProvider(LuckyNumberWidgetProvider::class)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -122,7 +122,7 @@ class MessageRepository @Inject constructor(
|
|||||||
fetch = {
|
fetch = {
|
||||||
wulkanowySdkFactory.create(student)
|
wulkanowySdkFactory.create(student)
|
||||||
.getMessageDetails(
|
.getMessageDetails(
|
||||||
messageKey = it!!.message.messageGlobalKey,
|
messageKey = message.messageGlobalKey,
|
||||||
markAsRead = message.unread && markAsRead,
|
markAsRead = message.unread && markAsRead,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
@ -38,7 +38,7 @@ class MobileDeviceRepository @Inject constructor(
|
|||||||
val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, student))
|
val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, student))
|
||||||
it.isEmpty() || forceRefresh || isExpired
|
it.isEmpty() || forceRefresh || isExpired
|
||||||
},
|
},
|
||||||
query = { mobileDb.loadAll(student.userLoginId) },
|
query = { mobileDb.loadAll(student.studentId) },
|
||||||
fetch = {
|
fetch = {
|
||||||
wulkanowySdkFactory.create(student, semester)
|
wulkanowySdkFactory.create(student, semester)
|
||||||
.getRegisteredDevices()
|
.getRegisteredDevices()
|
||||||
|
@ -10,9 +10,11 @@ import com.fredporciuncula.flow.preferences.Serializer
|
|||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import io.github.wulkanowy.R
|
import io.github.wulkanowy.R
|
||||||
import io.github.wulkanowy.data.enums.AppTheme
|
import io.github.wulkanowy.data.enums.AppTheme
|
||||||
|
import io.github.wulkanowy.data.enums.AttendanceCalculatorSortingMode
|
||||||
import io.github.wulkanowy.data.enums.GradeColorTheme
|
import io.github.wulkanowy.data.enums.GradeColorTheme
|
||||||
import io.github.wulkanowy.data.enums.GradeExpandMode
|
import io.github.wulkanowy.data.enums.GradeExpandMode
|
||||||
import io.github.wulkanowy.data.enums.GradeSortingMode
|
import io.github.wulkanowy.data.enums.GradeSortingMode
|
||||||
|
import io.github.wulkanowy.data.enums.ShowAdditionalLessonsMode
|
||||||
import io.github.wulkanowy.data.enums.TimetableGapsMode
|
import io.github.wulkanowy.data.enums.TimetableGapsMode
|
||||||
import io.github.wulkanowy.data.enums.TimetableMode
|
import io.github.wulkanowy.data.enums.TimetableMode
|
||||||
import io.github.wulkanowy.ui.modules.dashboard.DashboardItem
|
import io.github.wulkanowy.ui.modules.dashboard.DashboardItem
|
||||||
@ -41,6 +43,27 @@ class PreferencesRepository @Inject constructor(
|
|||||||
R.bool.pref_default_attendance_present
|
R.bool.pref_default_attendance_present
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val targetAttendanceFlow: Flow<Int>
|
||||||
|
get() = flowSharedPref.getInt(
|
||||||
|
context.getString(R.string.pref_key_attendance_target),
|
||||||
|
context.resources.getInteger(R.integer.pref_default_attendance_target)
|
||||||
|
).asFlow()
|
||||||
|
|
||||||
|
val attendanceCalculatorSortingModeFlow: Flow<AttendanceCalculatorSortingMode>
|
||||||
|
get() = flowSharedPref.getString(
|
||||||
|
context.getString(R.string.pref_key_attendance_calculator_sorting_mode),
|
||||||
|
context.resources.getString(R.string.pref_default_attendance_calculator_sorting_mode)
|
||||||
|
).asFlow().map(AttendanceCalculatorSortingMode::getByValue)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subjects are empty when they don't have any attendances (total = 0, attendances = 0, absences = 0).
|
||||||
|
*/
|
||||||
|
val attendanceCalculatorShowEmptySubjects: Flow<Boolean>
|
||||||
|
get() = flowSharedPref.getBoolean(
|
||||||
|
context.getString(R.string.pref_key_attendance_calculator_show_empty_subjects),
|
||||||
|
context.resources.getBoolean(R.bool.pref_default_attendance_calculator_show_empty_subjects)
|
||||||
|
).asFlow()
|
||||||
|
|
||||||
private val gradeAverageModePref: Preference<GradeAverageMode>
|
private val gradeAverageModePref: Preference<GradeAverageMode>
|
||||||
get() = getObjectFlow(
|
get() = getObjectFlow(
|
||||||
R.string.pref_key_grade_average_mode,
|
R.string.pref_key_grade_average_mode,
|
||||||
@ -191,6 +214,12 @@ class PreferencesRepository @Inject constructor(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val showAdditionalLessonsInPlan: ShowAdditionalLessonsMode
|
||||||
|
get() = getString(
|
||||||
|
R.string.pref_key_timetable_show_additional_lessons,
|
||||||
|
R.string.pref_default_timetable_show_additional_lessons
|
||||||
|
).let { ShowAdditionalLessonsMode.getByValue(it) }
|
||||||
|
|
||||||
val gradeSortingMode: GradeSortingMode
|
val gradeSortingMode: GradeSortingMode
|
||||||
get() = GradeSortingMode.getByValue(
|
get() = GradeSortingMode.getByValue(
|
||||||
getString(
|
getString(
|
||||||
|
@ -37,7 +37,7 @@ class SchoolAnnouncementRepository @Inject constructor(
|
|||||||
it.isEmpty() || forceRefresh || isExpired
|
it.isEmpty() || forceRefresh || isExpired
|
||||||
},
|
},
|
||||||
query = {
|
query = {
|
||||||
schoolAnnouncementDb.loadAll(student.userLoginId)
|
schoolAnnouncementDb.loadAll(student.studentId)
|
||||||
},
|
},
|
||||||
fetch = {
|
fetch = {
|
||||||
val sdk = wulkanowySdkFactory.create(student)
|
val sdk = wulkanowySdkFactory.create(student)
|
||||||
@ -57,7 +57,7 @@ class SchoolAnnouncementRepository @Inject constructor(
|
|||||||
)
|
)
|
||||||
|
|
||||||
fun getSchoolAnnouncementFromDatabase(student: Student): Flow<List<SchoolAnnouncement>> {
|
fun getSchoolAnnouncementFromDatabase(student: Student): Flow<List<SchoolAnnouncement>> {
|
||||||
return schoolAnnouncementDb.loadAll(student.userLoginId)
|
return schoolAnnouncementDb.loadAll(student.studentId)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun updateSchoolAnnouncement(schoolAnnouncement: List<SchoolAnnouncement>) =
|
suspend fun updateSchoolAnnouncement(schoolAnnouncement: List<SchoolAnnouncement>) =
|
||||||
|
@ -64,7 +64,10 @@ class SemesterRepository @Inject constructor(
|
|||||||
.getSemesters()
|
.getSemesters()
|
||||||
.mapToEntities(student.studentId)
|
.mapToEntities(student.studentId)
|
||||||
|
|
||||||
if (new.isEmpty()) return Timber.i("Empty semester list!")
|
if (new.isEmpty()) {
|
||||||
|
Timber.i("Empty semester list from SDK!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val old = semesterDb.loadAll(student.studentId, student.classId)
|
val old = semesterDb.loadAll(student.studentId, student.classId)
|
||||||
semesterDb.removeOldAndSaveNew(
|
semesterDb.removeOldAndSaveNew(
|
||||||
|
@ -42,6 +42,7 @@ class StudentRepository @Inject constructor(
|
|||||||
): RegisterUser = wulkanowySdkFactory.create()
|
): RegisterUser = wulkanowySdkFactory.create()
|
||||||
.getStudentsFromHebe(token, pin, symbol, "")
|
.getStudentsFromHebe(token, pin, symbol, "")
|
||||||
.mapToPojo(null)
|
.mapToPojo(null)
|
||||||
|
.also { it.logErrors() }
|
||||||
|
|
||||||
suspend fun getUserSubjectsFromScrapper(
|
suspend fun getUserSubjectsFromScrapper(
|
||||||
email: String,
|
email: String,
|
||||||
@ -52,6 +53,7 @@ class StudentRepository @Inject constructor(
|
|||||||
): RegisterUser = wulkanowySdkFactory.create()
|
): RegisterUser = wulkanowySdkFactory.create()
|
||||||
.getUserSubjectsFromScrapper(email, password, scrapperBaseUrl, domainSuffix, symbol)
|
.getUserSubjectsFromScrapper(email, password, scrapperBaseUrl, domainSuffix, symbol)
|
||||||
.mapToPojo(password)
|
.mapToPojo(password)
|
||||||
|
.also { it.logErrors() }
|
||||||
|
|
||||||
suspend fun getStudentsHybrid(
|
suspend fun getStudentsHybrid(
|
||||||
email: String,
|
email: String,
|
||||||
@ -61,6 +63,7 @@ class StudentRepository @Inject constructor(
|
|||||||
): RegisterUser = wulkanowySdkFactory.create()
|
): RegisterUser = wulkanowySdkFactory.create()
|
||||||
.getStudentsHybrid(email, password, scrapperBaseUrl, "", symbol)
|
.getStudentsHybrid(email, password, scrapperBaseUrl, "", symbol)
|
||||||
.mapToPojo(password)
|
.mapToPojo(password)
|
||||||
|
.also { it.logErrors() }
|
||||||
|
|
||||||
suspend fun getSavedStudents(decryptPass: Boolean = true): List<StudentWithSemesters> {
|
suspend fun getSavedStudents(decryptPass: Boolean = true): List<StudentWithSemesters> {
|
||||||
return studentDb.loadStudentsWithSemesters().map { (student, semesters) ->
|
return studentDb.loadStudentsWithSemesters().map { (student, semesters) ->
|
||||||
@ -195,10 +198,10 @@ class StudentRepository @Inject constructor(
|
|||||||
.authorizePermission(pesel)
|
.authorizePermission(pesel)
|
||||||
|
|
||||||
suspend fun refreshStudentAfterAuthorize(student: Student, semester: Semester) {
|
suspend fun refreshStudentAfterAuthorize(student: Student, semester: Semester) {
|
||||||
val newCurrentApiStudent = wulkanowySdkFactory
|
val wulkanowySdk = wulkanowySdkFactory.create(student, semester)
|
||||||
.create(student, semester)
|
val newCurrentApiStudent = runCatching { wulkanowySdk.getCurrentStudent() }
|
||||||
.getCurrentStudent()
|
.onFailure { Timber.e(it, "Can't find student with id ${student.studentId}") }
|
||||||
?: return Timber.d("Can't find student with id ${student.studentId}")
|
.getOrNull() ?: return
|
||||||
|
|
||||||
val studentName = StudentName(
|
val studentName = StudentName(
|
||||||
studentName = "${newCurrentApiStudent.studentName} ${newCurrentApiStudent.studentSurname}"
|
studentName = "${newCurrentApiStudent.studentName} ${newCurrentApiStudent.studentSurname}"
|
||||||
@ -221,6 +224,18 @@ class StudentRepository @Inject constructor(
|
|||||||
appDatabase.clearAllTables()
|
appDatabase.clearAllTables()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun RegisterUser.logErrors() {
|
||||||
|
val symbolsErrors = symbols.filter { it.error != null }
|
||||||
|
.map { it.error }
|
||||||
|
val unitsErrors = symbols.flatMap { it.schools }
|
||||||
|
.filter { it.error != null }
|
||||||
|
.map { it.error }
|
||||||
|
|
||||||
|
(symbolsErrors + unitsErrors).forEach { error ->
|
||||||
|
Timber.e(error, "Error occurred while fetching students")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class NoAuthorizationException : Exception()
|
class NoAuthorizationException : Exception()
|
||||||
|
@ -13,6 +13,8 @@ import io.github.wulkanowy.data.mappers.mapToEntities
|
|||||||
import io.github.wulkanowy.data.networkBoundResource
|
import io.github.wulkanowy.data.networkBoundResource
|
||||||
import io.github.wulkanowy.data.pojos.TimetableFull
|
import io.github.wulkanowy.data.pojos.TimetableFull
|
||||||
import io.github.wulkanowy.services.alarm.TimetableNotificationSchedulerHelper
|
import io.github.wulkanowy.services.alarm.TimetableNotificationSchedulerHelper
|
||||||
|
import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetProvider
|
||||||
|
import io.github.wulkanowy.utils.AppWidgetUpdater
|
||||||
import io.github.wulkanowy.utils.AutoRefreshHelper
|
import io.github.wulkanowy.utils.AutoRefreshHelper
|
||||||
import io.github.wulkanowy.utils.getRefreshKey
|
import io.github.wulkanowy.utils.getRefreshKey
|
||||||
import io.github.wulkanowy.utils.monday
|
import io.github.wulkanowy.utils.monday
|
||||||
@ -26,6 +28,7 @@ import java.time.LocalDate
|
|||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class TimetableRepository @Inject constructor(
|
class TimetableRepository @Inject constructor(
|
||||||
private val timetableDb: TimetableDao,
|
private val timetableDb: TimetableDao,
|
||||||
@ -34,6 +37,7 @@ class TimetableRepository @Inject constructor(
|
|||||||
private val wulkanowySdkFactory: WulkanowySdkFactory,
|
private val wulkanowySdkFactory: WulkanowySdkFactory,
|
||||||
private val schedulerHelper: TimetableNotificationSchedulerHelper,
|
private val schedulerHelper: TimetableNotificationSchedulerHelper,
|
||||||
private val refreshHelper: AutoRefreshHelper,
|
private val refreshHelper: AutoRefreshHelper,
|
||||||
|
private val appWidgetUpdater: AppWidgetUpdater,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val saveFetchResultMutex = Mutex()
|
private val saveFetchResultMutex = Mutex()
|
||||||
@ -52,7 +56,8 @@ class TimetableRepository @Inject constructor(
|
|||||||
forceRefresh: Boolean,
|
forceRefresh: Boolean,
|
||||||
refreshAdditional: Boolean = false,
|
refreshAdditional: Boolean = false,
|
||||||
notify: Boolean = false,
|
notify: Boolean = false,
|
||||||
timetableType: TimetableType = TimetableType.NORMAL
|
timetableType: TimetableType = TimetableType.NORMAL,
|
||||||
|
isFromAppWidget: Boolean = false
|
||||||
) = networkBoundResource(
|
) = networkBoundResource(
|
||||||
mutex = saveFetchResultMutex,
|
mutex = saveFetchResultMutex,
|
||||||
isResultEmpty = {
|
isResultEmpty = {
|
||||||
@ -83,6 +88,9 @@ class TimetableRepository @Inject constructor(
|
|||||||
refreshDayHeaders(timetableOld.headers, timetableNew.headers)
|
refreshDayHeaders(timetableOld.headers, timetableNew.headers)
|
||||||
|
|
||||||
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester, start, end))
|
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester, start, end))
|
||||||
|
if (!isFromAppWidget) {
|
||||||
|
appWidgetUpdater.updateAllAppWidgetsByProvider(TimetableWidgetProvider::class)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
filterResult = { (timetable, additional, headers) ->
|
filterResult = { (timetable, additional, headers) ->
|
||||||
TimetableFull(
|
TimetableFull(
|
||||||
|
@ -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 }
|
||||||
|
}
|
||||||
|
}
|
@ -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 {
|
private fun String.getUnauthorizedVersion(): String {
|
||||||
return normalizeStudentName().split(" ")
|
return normalizeStudentName().split(" ")
|
||||||
.joinToString(" ") {
|
.joinToString(" ") {
|
||||||
it.first() + "*".repeat(it.length - 1)
|
it.firstOrNull()?.toString().orEmpty() + "*".repeat((it.length - 1).coerceAtLeast(0))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,7 @@ import io.github.wulkanowy.data.db.entities.Attendance
|
|||||||
import io.github.wulkanowy.databinding.DialogExcuseBinding
|
import io.github.wulkanowy.databinding.DialogExcuseBinding
|
||||||
import io.github.wulkanowy.databinding.FragmentAttendanceBinding
|
import io.github.wulkanowy.databinding.FragmentAttendanceBinding
|
||||||
import io.github.wulkanowy.ui.base.BaseFragment
|
import io.github.wulkanowy.ui.base.BaseFragment
|
||||||
|
import io.github.wulkanowy.ui.modules.attendance.calculator.AttendanceCalculatorFragment
|
||||||
import io.github.wulkanowy.ui.modules.attendance.summary.AttendanceSummaryFragment
|
import io.github.wulkanowy.ui.modules.attendance.summary.AttendanceSummaryFragment
|
||||||
import io.github.wulkanowy.ui.modules.main.MainActivity
|
import io.github.wulkanowy.ui.modules.main.MainActivity
|
||||||
import io.github.wulkanowy.ui.modules.main.MainView
|
import io.github.wulkanowy.ui.modules.main.MainView
|
||||||
@ -134,6 +135,7 @@ class AttendanceFragment : BaseFragment<FragmentAttendanceBinding>(R.layout.frag
|
|||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
return if (item.itemId == R.id.attendanceMenuSummary) presenter.onSummarySwitchSelected()
|
return if (item.itemId == R.id.attendanceMenuSummary) presenter.onSummarySwitchSelected()
|
||||||
|
else if (item.itemId == R.id.attendanceMenuCalculator) presenter.onCalculatorSwitchSelected()
|
||||||
else false
|
else false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -253,6 +255,10 @@ class AttendanceFragment : BaseFragment<FragmentAttendanceBinding>(R.layout.frag
|
|||||||
(activity as? MainActivity)?.pushView(AttendanceSummaryFragment.newInstance())
|
(activity as? MainActivity)?.pushView(AttendanceSummaryFragment.newInstance())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun openCalculatorView() {
|
||||||
|
(activity as? MainActivity)?.pushView(AttendanceCalculatorFragment.newInstance())
|
||||||
|
}
|
||||||
|
|
||||||
override fun startActionMode() {
|
override fun startActionMode() {
|
||||||
actionMode = (activity as MainActivity?)?.startSupportActionMode(actionModeCallback)
|
actionMode = (activity as MainActivity?)?.startSupportActionMode(actionModeCallback)
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,36 @@
|
|||||||
package io.github.wulkanowy.ui.modules.attendance
|
package io.github.wulkanowy.ui.modules.attendance
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import io.github.wulkanowy.data.*
|
import io.github.wulkanowy.data.Resource
|
||||||
import io.github.wulkanowy.data.db.entities.Attendance
|
import io.github.wulkanowy.data.db.entities.Attendance
|
||||||
import io.github.wulkanowy.data.db.entities.Semester
|
import io.github.wulkanowy.data.db.entities.Semester
|
||||||
|
import io.github.wulkanowy.data.flatResourceFlow
|
||||||
|
import io.github.wulkanowy.data.logResourceStatus
|
||||||
|
import io.github.wulkanowy.data.mapResourceData
|
||||||
|
import io.github.wulkanowy.data.onResourceData
|
||||||
|
import io.github.wulkanowy.data.onResourceError
|
||||||
|
import io.github.wulkanowy.data.onResourceIntermediate
|
||||||
|
import io.github.wulkanowy.data.onResourceLoading
|
||||||
|
import io.github.wulkanowy.data.onResourceNotLoading
|
||||||
|
import io.github.wulkanowy.data.onResourceSuccess
|
||||||
import io.github.wulkanowy.data.repositories.AttendanceRepository
|
import io.github.wulkanowy.data.repositories.AttendanceRepository
|
||||||
import io.github.wulkanowy.data.repositories.PreferencesRepository
|
import io.github.wulkanowy.data.repositories.PreferencesRepository
|
||||||
import io.github.wulkanowy.data.repositories.SemesterRepository
|
import io.github.wulkanowy.data.repositories.SemesterRepository
|
||||||
import io.github.wulkanowy.data.repositories.StudentRepository
|
import io.github.wulkanowy.data.repositories.StudentRepository
|
||||||
|
import io.github.wulkanowy.data.resourceFlow
|
||||||
import io.github.wulkanowy.ui.base.BasePresenter
|
import io.github.wulkanowy.ui.base.BasePresenter
|
||||||
import io.github.wulkanowy.ui.base.ErrorHandler
|
import io.github.wulkanowy.ui.base.ErrorHandler
|
||||||
import io.github.wulkanowy.utils.*
|
import io.github.wulkanowy.utils.AnalyticsHelper
|
||||||
|
import io.github.wulkanowy.utils.capitalise
|
||||||
|
import io.github.wulkanowy.utils.getLastSchoolDayIfHoliday
|
||||||
|
import io.github.wulkanowy.utils.isExcusableOrNotExcused
|
||||||
|
import io.github.wulkanowy.utils.isHolidays
|
||||||
|
import io.github.wulkanowy.utils.monday
|
||||||
|
import io.github.wulkanowy.utils.nextSchoolDay
|
||||||
|
import io.github.wulkanowy.utils.previousOrSameSchoolDay
|
||||||
|
import io.github.wulkanowy.utils.previousSchoolDay
|
||||||
|
import io.github.wulkanowy.utils.sunday
|
||||||
|
import io.github.wulkanowy.utils.toFormattedString
|
||||||
import kotlinx.coroutines.flow.firstOrNull
|
import kotlinx.coroutines.flow.firstOrNull
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
@ -195,6 +215,11 @@ class AttendancePresenter @Inject constructor(
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun onCalculatorSwitchSelected(): Boolean {
|
||||||
|
view?.openCalculatorView()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
private fun loadData(forceRefresh: Boolean = false) {
|
private fun loadData(forceRefresh: Boolean = false) {
|
||||||
Timber.i("Loading attendance data started")
|
Timber.i("Loading attendance data started")
|
||||||
|
|
||||||
|
@ -56,6 +56,8 @@ interface AttendanceView : BaseView {
|
|||||||
|
|
||||||
fun openSummaryView()
|
fun openSummaryView()
|
||||||
|
|
||||||
|
fun openCalculatorView()
|
||||||
|
|
||||||
fun startSendMessageIntent(date: LocalDate, numbers: String, reason: String)
|
fun startSendMessageIntent(date: LocalDate, numbers: String, reason: String)
|
||||||
|
|
||||||
fun startActionMode()
|
fun startActionMode()
|
||||||
|
@ -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()
|
||||||
|
}
|
@ -27,8 +27,12 @@ class AuthPresenter @Inject constructor(
|
|||||||
|
|
||||||
private fun loadName() {
|
private fun loadName() {
|
||||||
presenterScope.launch {
|
presenterScope.launch {
|
||||||
runCatching { studentRepository.getCurrentStudent(false) }
|
runCatching {
|
||||||
.onSuccess { view?.showDescriptionWithName(it.studentName) }
|
studentRepository.getCurrentStudent(false)
|
||||||
|
.studentName
|
||||||
|
.replace(" ", "\u00A0")
|
||||||
|
}
|
||||||
|
.onSuccess { view?.showDescriptionWithName(it) }
|
||||||
.onFailure { errorHandler.dispatch(it) }
|
.onFailure { errorHandler.dispatch(it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,5 +26,7 @@ private fun generateSummary(subject: String, predicted: String, final: String) =
|
|||||||
proposedPoints = "",
|
proposedPoints = "",
|
||||||
finalPoints = "",
|
finalPoints = "",
|
||||||
pointsSum = "",
|
pointsSum = "",
|
||||||
average = .0
|
average = .0,
|
||||||
|
pointsSumAllYear = null,
|
||||||
|
averageAllYear = null,
|
||||||
)
|
)
|
||||||
|
@ -19,6 +19,6 @@ val debugSchoolAnnouncementItems = listOf(
|
|||||||
private fun generateAnnouncement(subject: String, content: String) = SchoolAnnouncement(
|
private fun generateAnnouncement(subject: String, content: String) = SchoolAnnouncement(
|
||||||
subject = subject,
|
subject = subject,
|
||||||
content = content,
|
content = content,
|
||||||
userLoginId = 0,
|
studentId = 0,
|
||||||
date = LocalDate.now()
|
date = LocalDate.now()
|
||||||
)
|
)
|
||||||
|
@ -266,7 +266,9 @@ class GradeAverageProvider @Inject constructor(
|
|||||||
proposedPoints = "",
|
proposedPoints = "",
|
||||||
finalPoints = "",
|
finalPoints = "",
|
||||||
pointsSum = "",
|
pointsSum = "",
|
||||||
average = .0
|
pointsSumAllYear = null,
|
||||||
|
average = .0,
|
||||||
|
averageAllYear = null,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -294,13 +296,15 @@ class GradeAverageProvider @Inject constructor(
|
|||||||
proposedPoints = "",
|
proposedPoints = "",
|
||||||
finalPoints = "",
|
finalPoints = "",
|
||||||
pointsSum = "",
|
pointsSum = "",
|
||||||
|
pointsSumAllYear = null,
|
||||||
average = when {
|
average = when {
|
||||||
calcAverage -> details
|
calcAverage -> details
|
||||||
.updateModifiers(student, params)
|
.updateModifiers(student, params)
|
||||||
.calcAverage(isOptionalArithmeticAverage = params.isOptionalArithmeticAverage)
|
.calcAverage(isOptionalArithmeticAverage = params.isOptionalArithmeticAverage)
|
||||||
|
|
||||||
else -> .0
|
else -> .0
|
||||||
}
|
},
|
||||||
|
averageAllYear = null,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -96,9 +96,11 @@ class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter<Recycler
|
|||||||
ViewType.HEADER.id -> HeaderViewHolder(
|
ViewType.HEADER.id -> HeaderViewHolder(
|
||||||
HeaderGradeDetailsBinding.inflate(inflater, parent, false)
|
HeaderGradeDetailsBinding.inflate(inflater, parent, false)
|
||||||
)
|
)
|
||||||
|
|
||||||
ViewType.ITEM.id -> ItemViewHolder(
|
ViewType.ITEM.id -> ItemViewHolder(
|
||||||
ItemGradeDetailsBinding.inflate(inflater, parent, false)
|
ItemGradeDetailsBinding.inflate(inflater, parent, false)
|
||||||
)
|
)
|
||||||
|
|
||||||
else -> throw IllegalStateException()
|
else -> throw IllegalStateException()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -110,6 +112,7 @@ class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter<Recycler
|
|||||||
header = items[position].value as GradeDetailsHeader,
|
header = items[position].value as GradeDetailsHeader,
|
||||||
position = position
|
position = position
|
||||||
)
|
)
|
||||||
|
|
||||||
is ItemViewHolder -> bindItemViewHolder(
|
is ItemViewHolder -> bindItemViewHolder(
|
||||||
holder = holder,
|
holder = holder,
|
||||||
grade = items[position].value as Grade
|
grade = items[position].value as Grade
|
||||||
@ -133,6 +136,10 @@ class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter<Recycler
|
|||||||
maxLines = if (expandedPositions[headerPosition]) 2 else 1
|
maxLines = if (expandedPositions[headerPosition]) 2 else 1
|
||||||
}
|
}
|
||||||
gradeHeaderAverage.text = formatAverage(header.average, root.context.resources)
|
gradeHeaderAverage.text = formatAverage(header.average, root.context.resources)
|
||||||
|
with(gradeHeaderAverageAllYear) {
|
||||||
|
isVisible = header.averageAllYear != null && header.averageAllYear != .0
|
||||||
|
text = formatAverageAllYear(header.averageAllYear, root.context.resources)
|
||||||
|
}
|
||||||
gradeHeaderPointsSum.text =
|
gradeHeaderPointsSum.text =
|
||||||
context.getString(R.string.grade_points_sum, header.pointsSum)
|
context.getString(R.string.grade_points_sum, header.pointsSum)
|
||||||
gradeHeaderPointsSum.isVisible = !header.pointsSum.isNullOrEmpty()
|
gradeHeaderPointsSum.isVisible = !header.pointsSum.isNullOrEmpty()
|
||||||
@ -233,6 +240,13 @@ class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter<Recycler
|
|||||||
resources.getString(R.string.grade_average, average)
|
resources.getString(R.string.grade_average, average)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun formatAverageAllYear(average: Double?, resources: Resources) =
|
||||||
|
if (average == null || average == .0) {
|
||||||
|
resources.getString(R.string.grade_no_average)
|
||||||
|
} else {
|
||||||
|
resources.getString(R.string.grade_average_year, average)
|
||||||
|
}
|
||||||
|
|
||||||
private class HeaderViewHolder(val binding: HeaderGradeDetailsBinding) :
|
private class HeaderViewHolder(val binding: HeaderGradeDetailsBinding) :
|
||||||
RecyclerView.ViewHolder(binding.root)
|
RecyclerView.ViewHolder(binding.root)
|
||||||
|
|
||||||
|
@ -13,6 +13,7 @@ data class GradeDetailsItem(
|
|||||||
data class GradeDetailsHeader(
|
data class GradeDetailsHeader(
|
||||||
val subject: String,
|
val subject: String,
|
||||||
val average: Double?,
|
val average: Double?,
|
||||||
|
val averageAllYear: Double?,
|
||||||
val pointsSum: String?,
|
val pointsSum: String?,
|
||||||
val grades: List<GradeDetailsItem>
|
val grades: List<GradeDetailsItem>
|
||||||
) {
|
) {
|
||||||
|
@ -226,8 +226,9 @@ class GradeDetailsPresenter @Inject constructor(
|
|||||||
GradeDetailsHeader(
|
GradeDetailsHeader(
|
||||||
subject = gradeSubject.subject,
|
subject = gradeSubject.subject,
|
||||||
average = gradeSubject.average,
|
average = gradeSubject.average,
|
||||||
|
averageAllYear = gradeSubject.summary.averageAllYear,
|
||||||
pointsSum = gradeSubject.points,
|
pointsSum = gradeSubject.points,
|
||||||
grades = subItems
|
grades = subItems,
|
||||||
).apply {
|
).apply {
|
||||||
newGrades = gradeSubject.grades.filter { grade -> !grade.isRead }.size
|
newGrades = gradeSubject.grades.filter { grade -> !grade.isRead }.size
|
||||||
}, ViewType.HEADER
|
}, ViewType.HEADER
|
||||||
|
@ -3,6 +3,7 @@ package io.github.wulkanowy.ui.modules.grade.summary
|
|||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.view.isGone
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import io.github.wulkanowy.R
|
import io.github.wulkanowy.R
|
||||||
@ -65,37 +66,55 @@ class GradeSummaryAdapter @Inject constructor(
|
|||||||
val gradeSummaries = items
|
val gradeSummaries = items
|
||||||
.filter { it.gradeDescriptive == null }
|
.filter { it.gradeDescriptive == null }
|
||||||
.map { it.gradeSummary }
|
.map { it.gradeSummary }
|
||||||
|
val isSecondSemester = items.any { item ->
|
||||||
|
item.gradeSummary.let { it.averageAllYear != null && it.averageAllYear != .0 }
|
||||||
|
}
|
||||||
|
|
||||||
val context = binding.root.context
|
val context = binding.root.context
|
||||||
val finalItemsCount = gradeSummaries.count { isGradeValid(it.finalGrade) }
|
val finalItemsCount = gradeSummaries.count { isGradeValid(it.finalGrade) }
|
||||||
val calculatedItemsCount = gradeSummaries.count { value -> value.average != 0.0 }
|
val calculatedSemesterItemsCount = gradeSummaries.count { value -> value.average != 0.0 }
|
||||||
|
val calculatedAnnualItemsCount =
|
||||||
|
gradeSummaries.count { value -> value.averageAllYear != 0.0 }
|
||||||
val allItemsCount = gradeSummaries.count { !it.subject.equals("zachowanie", true) }
|
val allItemsCount = gradeSummaries.count { !it.subject.equals("zachowanie", true) }
|
||||||
val finalAverage = gradeSummaries.calcFinalAverage(
|
val finalAverage = gradeSummaries.calcFinalAverage(
|
||||||
preferencesRepository.gradePlusModifier,
|
plusModifier = preferencesRepository.gradePlusModifier,
|
||||||
preferencesRepository.gradeMinusModifier
|
minusModifier = preferencesRepository.gradeMinusModifier,
|
||||||
)
|
)
|
||||||
val calculatedAverage = gradeSummaries.filter { value -> value.average != 0.0 }
|
val calculatedSemesterAverage = gradeSummaries.filter { value -> value.average != 0.0 }
|
||||||
.map { values -> values.average }
|
.map { values -> values.average }
|
||||||
.reversed() // fix average precision
|
.reversed() // fix average precision
|
||||||
.average()
|
.average()
|
||||||
.let { if (it.isNaN()) 0.0 else it }
|
.let { if (it.isNaN()) 0.0 else it }
|
||||||
|
val calculatedAnnualAverage = gradeSummaries.filter { value -> value.averageAllYear != 0.0 }
|
||||||
|
.mapNotNull { values -> values.averageAllYear }
|
||||||
|
.reversed() // fix average precision
|
||||||
|
.average()
|
||||||
|
.let { if (it.isNaN()) 0.0 else it }
|
||||||
|
|
||||||
with(binding) {
|
with(binding) {
|
||||||
|
gradeSummaryScrollableHeaderCalculated.text = formatAverage(calculatedSemesterAverage)
|
||||||
|
gradeSummaryScrollableHeaderCalculatedAnnual.text =
|
||||||
|
formatAverage(calculatedAnnualAverage)
|
||||||
gradeSummaryScrollableHeaderFinal.text = formatAverage(finalAverage)
|
gradeSummaryScrollableHeaderFinal.text = formatAverage(finalAverage)
|
||||||
gradeSummaryScrollableHeaderCalculated.text = formatAverage(calculatedAverage)
|
gradeSummaryScrollableHeaderFinalSubjectCount.text = context.getString(
|
||||||
gradeSummaryScrollableHeaderFinalSubjectCount.text =
|
|
||||||
context.getString(
|
|
||||||
R.string.grade_summary_from_subjects,
|
|
||||||
finalItemsCount,
|
|
||||||
allItemsCount
|
|
||||||
)
|
|
||||||
gradeSummaryScrollableHeaderCalculatedSubjectCount.text = context.getString(
|
|
||||||
R.string.grade_summary_from_subjects,
|
R.string.grade_summary_from_subjects,
|
||||||
calculatedItemsCount,
|
finalItemsCount,
|
||||||
allItemsCount
|
allItemsCount
|
||||||
)
|
)
|
||||||
|
gradeSummaryScrollableHeaderCalculatedSubjectCount.text = context.getString(
|
||||||
|
R.string.grade_summary_from_subjects,
|
||||||
|
calculatedSemesterItemsCount,
|
||||||
|
allItemsCount
|
||||||
|
)
|
||||||
|
gradeSummaryScrollableHeaderCalculatedSubjectCountAnnual.text = context.getString(
|
||||||
|
R.string.grade_summary_from_subjects,
|
||||||
|
calculatedAnnualItemsCount,
|
||||||
|
allItemsCount
|
||||||
|
)
|
||||||
|
gradeSummaryScrollableHeaderCalculatedAnnualContainer.isVisible = isSecondSemester
|
||||||
|
|
||||||
gradeSummaryCalculatedAverageHelp.setOnClickListener { onCalculatedHelpClickListener() }
|
gradeSummaryCalculatedAverageHelp.setOnClickListener { onCalculatedHelpClickListener() }
|
||||||
|
gradeSummaryCalculatedAverageHelpAnnual.setOnClickListener { onCalculatedHelpClickListener() }
|
||||||
gradeSummaryFinalAverageHelp.setOnClickListener { onFinalHelpClickListener() }
|
gradeSummaryFinalAverageHelp.setOnClickListener { onFinalHelpClickListener() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -107,7 +126,12 @@ class GradeSummaryAdapter @Inject constructor(
|
|||||||
with(binding) {
|
with(binding) {
|
||||||
gradeSummaryItemTitle.text = gradeSummary.subject
|
gradeSummaryItemTitle.text = gradeSummary.subject
|
||||||
gradeSummaryItemPoints.text = gradeSummary.pointsSum
|
gradeSummaryItemPoints.text = gradeSummary.pointsSum
|
||||||
|
|
||||||
gradeSummaryItemAverage.text = formatAverage(gradeSummary.average, "")
|
gradeSummaryItemAverage.text = formatAverage(gradeSummary.average, "")
|
||||||
|
gradeSummaryItemAverageAllYear.text = gradeSummary.averageAllYear?.let {
|
||||||
|
formatAverage(it, "")
|
||||||
|
}
|
||||||
|
|
||||||
gradeSummaryItemPredicted.text =
|
gradeSummaryItemPredicted.text =
|
||||||
"${gradeSummary.predictedGrade} ${gradeSummary.proposedPoints}".trim()
|
"${gradeSummary.predictedGrade} ${gradeSummary.proposedPoints}".trim()
|
||||||
gradeSummaryItemFinal.text =
|
gradeSummaryItemFinal.text =
|
||||||
@ -116,6 +140,12 @@ class GradeSummaryAdapter @Inject constructor(
|
|||||||
root.context.getString(R.string.all_no_data)
|
root.context.getString(R.string.all_no_data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gradeSummaryItemAverageContainer.isVisible = gradeSummary.average != .0
|
||||||
|
gradeSummaryItemAverageDivider.isVisible = gradeSummaryItemAverageContainer.isVisible
|
||||||
|
gradeSummaryItemAverageAllYearContainer.isGone =
|
||||||
|
gradeSummary.averageAllYear == null || gradeSummary.averageAllYear == .0
|
||||||
|
gradeSummaryItemAverageAllYearDivider.isGone =
|
||||||
|
gradeSummaryItemAverageAllYearContainer.isGone
|
||||||
gradeSummaryItemFinalDivider.isVisible = gradeDescriptive == null
|
gradeSummaryItemFinalDivider.isVisible = gradeDescriptive == null
|
||||||
gradeSummaryItemPredictedDivider.isVisible = gradeDescriptive == null
|
gradeSummaryItemPredictedDivider.isVisible = gradeDescriptive == null
|
||||||
gradeSummaryItemPointsDivider.isVisible = gradeDescriptive == null
|
gradeSummaryItemPointsDivider.isVisible = gradeDescriptive == null
|
||||||
@ -123,6 +153,7 @@ class GradeSummaryAdapter @Inject constructor(
|
|||||||
gradeSummaryItemFinalContainer.isVisible = gradeDescriptive == null
|
gradeSummaryItemFinalContainer.isVisible = gradeDescriptive == null
|
||||||
gradeSummaryItemDescriptiveContainer.isVisible = gradeDescriptive != null
|
gradeSummaryItemDescriptiveContainer.isVisible = gradeDescriptive != null
|
||||||
gradeSummaryItemPointsContainer.isVisible = gradeSummary.pointsSum.isNotBlank()
|
gradeSummaryItemPointsContainer.isVisible = gradeSummary.pointsSum.isNotBlank()
|
||||||
|
gradeSummaryItemPointsDivider.isVisible = gradeSummaryItemPointsContainer.isVisible
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,19 +19,23 @@ class LoginStudentSelectAdapter @Inject constructor() :
|
|||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||||
val inflater = LayoutInflater.from(parent.context)
|
val inflater = LayoutInflater.from(parent.context)
|
||||||
return when (LoginStudentSelectItemType.values()[viewType]) {
|
return when (LoginStudentSelectItemType.entries[viewType]) {
|
||||||
LoginStudentSelectItemType.EMPTY_SYMBOLS_HEADER -> EmptySymbolsHeaderViewHolder(
|
LoginStudentSelectItemType.EMPTY_SYMBOLS_HEADER -> EmptySymbolsHeaderViewHolder(
|
||||||
ItemLoginStudentSelectEmptySymbolHeaderBinding.inflate(inflater, parent, false),
|
ItemLoginStudentSelectEmptySymbolHeaderBinding.inflate(inflater, parent, false),
|
||||||
)
|
)
|
||||||
|
|
||||||
LoginStudentSelectItemType.SYMBOL_HEADER -> SymbolsHeaderViewHolder(
|
LoginStudentSelectItemType.SYMBOL_HEADER -> SymbolsHeaderViewHolder(
|
||||||
ItemLoginStudentSelectHeaderSymbolBinding.inflate(inflater, parent, false)
|
ItemLoginStudentSelectHeaderSymbolBinding.inflate(inflater, parent, false)
|
||||||
)
|
)
|
||||||
|
|
||||||
LoginStudentSelectItemType.SCHOOL_HEADER -> SchoolHeaderViewHolder(
|
LoginStudentSelectItemType.SCHOOL_HEADER -> SchoolHeaderViewHolder(
|
||||||
ItemLoginStudentSelectHeaderSchoolBinding.inflate(inflater, parent, false)
|
ItemLoginStudentSelectHeaderSchoolBinding.inflate(inflater, parent, false)
|
||||||
)
|
)
|
||||||
|
|
||||||
LoginStudentSelectItemType.STUDENT -> StudentViewHolder(
|
LoginStudentSelectItemType.STUDENT -> StudentViewHolder(
|
||||||
ItemLoginStudentSelectStudentBinding.inflate(inflater, parent, false)
|
ItemLoginStudentSelectStudentBinding.inflate(inflater, parent, false)
|
||||||
)
|
)
|
||||||
|
|
||||||
LoginStudentSelectItemType.HELP -> HelpViewHolder(
|
LoginStudentSelectItemType.HELP -> HelpViewHolder(
|
||||||
ItemLoginStudentSelectHelpBinding.inflate(inflater, parent, false)
|
ItemLoginStudentSelectHelpBinding.inflate(inflater, parent, false)
|
||||||
)
|
)
|
||||||
@ -98,9 +102,11 @@ class LoginStudentSelectAdapter @Inject constructor() :
|
|||||||
with(binding) {
|
with(binding) {
|
||||||
loginStudentSelectHeaderSchoolName.text = buildString {
|
loginStudentSelectHeaderSchoolName.text = buildString {
|
||||||
append(item.unit.schoolName.trim())
|
append(item.unit.schoolName.trim())
|
||||||
append(" (")
|
if (item.unit.schoolShortName.isNotBlank()) {
|
||||||
append(item.unit.schoolShortName)
|
append(" (")
|
||||||
append(")")
|
append(item.unit.schoolShortName)
|
||||||
|
append(")")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
loginStudentSelectHeaderSchoolDetails.isVisible = item.unit.students.isEmpty()
|
loginStudentSelectHeaderSchoolDetails.isVisible = item.unit.students.isEmpty()
|
||||||
loginStudentSelectHeaderSchoolError.text = item.unit.error?.message
|
loginStudentSelectHeaderSchoolError.text = item.unit.error?.message
|
||||||
@ -170,9 +176,11 @@ class LoginStudentSelectAdapter @Inject constructor() :
|
|||||||
oldItem is LoginStudentSelectItem.SymbolHeader && newItem is LoginStudentSelectItem.SymbolHeader -> {
|
oldItem is LoginStudentSelectItem.SymbolHeader && newItem is LoginStudentSelectItem.SymbolHeader -> {
|
||||||
oldItem.symbol == newItem.symbol
|
oldItem.symbol == newItem.symbol
|
||||||
}
|
}
|
||||||
|
|
||||||
oldItem is LoginStudentSelectItem.Student && newItem is LoginStudentSelectItem.Student -> {
|
oldItem is LoginStudentSelectItem.Student && newItem is LoginStudentSelectItem.Student -> {
|
||||||
oldItem.student == newItem.student
|
oldItem.student == newItem.student
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> oldItem == newItem
|
else -> oldItem == newItem
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,10 +6,12 @@ import androidx.core.os.bundleOf
|
|||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import io.github.wulkanowy.R
|
import io.github.wulkanowy.R
|
||||||
|
import io.github.wulkanowy.data.db.entities.AdminMessage
|
||||||
import io.github.wulkanowy.data.pojos.RegisterUser
|
import io.github.wulkanowy.data.pojos.RegisterUser
|
||||||
import io.github.wulkanowy.data.repositories.PreferencesRepository
|
import io.github.wulkanowy.data.repositories.PreferencesRepository
|
||||||
import io.github.wulkanowy.databinding.FragmentLoginStudentSelectBinding
|
import io.github.wulkanowy.databinding.FragmentLoginStudentSelectBinding
|
||||||
import io.github.wulkanowy.ui.base.BaseFragment
|
import io.github.wulkanowy.ui.base.BaseFragment
|
||||||
|
import io.github.wulkanowy.ui.modules.dashboard.viewholders.AdminMessageViewHolder
|
||||||
import io.github.wulkanowy.ui.modules.login.LoginActivity
|
import io.github.wulkanowy.ui.modules.login.LoginActivity
|
||||||
import io.github.wulkanowy.ui.modules.login.LoginData
|
import io.github.wulkanowy.ui.modules.login.LoginData
|
||||||
import io.github.wulkanowy.ui.modules.login.support.LoginSupportDialog
|
import io.github.wulkanowy.ui.modules.login.support.LoginSupportDialog
|
||||||
@ -111,6 +113,19 @@ class LoginStudentSelectFragment :
|
|||||||
LoginSupportDialog.newInstance(supportInfo).show(childFragmentManager, "support_dialog")
|
LoginSupportDialog.newInstance(supportInfo).show(childFragmentManager, "support_dialog")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun showAdminMessage(adminMessage: AdminMessage?) {
|
||||||
|
AdminMessageViewHolder(
|
||||||
|
binding = binding.loginStudentSelectAdminMessage,
|
||||||
|
onAdminMessageDismissClickListener = presenter::onAdminMessageDismissed,
|
||||||
|
onAdminMessageClickListener = presenter::onAdminMessageSelected,
|
||||||
|
).bind(adminMessage)
|
||||||
|
binding.loginStudentSelectAdminMessage.root.isVisible = adminMessage != null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun openInternetBrowser(url: String) {
|
||||||
|
requireContext().openInternetBrowser(url)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
presenter.onDetachView()
|
presenter.onDetachView()
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
|
@ -2,16 +2,24 @@ package io.github.wulkanowy.ui.modules.login.studentselect
|
|||||||
|
|
||||||
import io.github.wulkanowy.data.Resource
|
import io.github.wulkanowy.data.Resource
|
||||||
import io.github.wulkanowy.data.dataOrNull
|
import io.github.wulkanowy.data.dataOrNull
|
||||||
|
import io.github.wulkanowy.data.db.entities.AdminMessage
|
||||||
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
|
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
|
||||||
|
import io.github.wulkanowy.data.enums.MessageType
|
||||||
|
import io.github.wulkanowy.data.flatResourceFlow
|
||||||
import io.github.wulkanowy.data.logResourceStatus
|
import io.github.wulkanowy.data.logResourceStatus
|
||||||
import io.github.wulkanowy.data.mappers.mapToStudentWithSemesters
|
import io.github.wulkanowy.data.mappers.mapToStudentWithSemesters
|
||||||
|
import io.github.wulkanowy.data.onResourceData
|
||||||
|
import io.github.wulkanowy.data.onResourceError
|
||||||
import io.github.wulkanowy.data.pojos.RegisterStudent
|
import io.github.wulkanowy.data.pojos.RegisterStudent
|
||||||
import io.github.wulkanowy.data.pojos.RegisterSymbol
|
import io.github.wulkanowy.data.pojos.RegisterSymbol
|
||||||
import io.github.wulkanowy.data.pojos.RegisterUnit
|
import io.github.wulkanowy.data.pojos.RegisterUnit
|
||||||
import io.github.wulkanowy.data.pojos.RegisterUser
|
import io.github.wulkanowy.data.pojos.RegisterUser
|
||||||
|
import io.github.wulkanowy.data.repositories.PreferencesRepository
|
||||||
import io.github.wulkanowy.data.repositories.SchoolsRepository
|
import io.github.wulkanowy.data.repositories.SchoolsRepository
|
||||||
import io.github.wulkanowy.data.repositories.StudentRepository
|
import io.github.wulkanowy.data.repositories.StudentRepository
|
||||||
import io.github.wulkanowy.data.resourceFlow
|
import io.github.wulkanowy.data.resourceFlow
|
||||||
|
import io.github.wulkanowy.domain.adminmessage.GetAppropriateAdminMessageUseCase
|
||||||
|
import io.github.wulkanowy.sdk.scrapper.exception.StudentGraduateException
|
||||||
import io.github.wulkanowy.sdk.scrapper.login.InvalidSymbolException
|
import io.github.wulkanowy.sdk.scrapper.login.InvalidSymbolException
|
||||||
import io.github.wulkanowy.services.sync.SyncManager
|
import io.github.wulkanowy.services.sync.SyncManager
|
||||||
import io.github.wulkanowy.ui.base.BasePresenter
|
import io.github.wulkanowy.ui.base.BasePresenter
|
||||||
@ -32,6 +40,8 @@ class LoginStudentSelectPresenter @Inject constructor(
|
|||||||
private val syncManager: SyncManager,
|
private val syncManager: SyncManager,
|
||||||
private val analytics: AnalyticsHelper,
|
private val analytics: AnalyticsHelper,
|
||||||
private val appInfo: AppInfo,
|
private val appInfo: AppInfo,
|
||||||
|
private val preferencesRepository: PreferencesRepository,
|
||||||
|
private val getAppropriateAdminMessageUseCase: GetAppropriateAdminMessageUseCase
|
||||||
) : BasePresenter<LoginStudentSelectView>(loginErrorHandler, studentRepository) {
|
) : BasePresenter<LoginStudentSelectView>(loginErrorHandler, studentRepository) {
|
||||||
|
|
||||||
private var lastError: Throwable? = null
|
private var lastError: Throwable? = null
|
||||||
@ -64,6 +74,7 @@ class LoginStudentSelectPresenter @Inject constructor(
|
|||||||
this.loginData = loginData
|
this.loginData = loginData
|
||||||
this.registerUser = registerUser
|
this.registerUser = registerUser
|
||||||
loadData()
|
loadData()
|
||||||
|
loadAdminMessage()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadData() {
|
private fun loadData() {
|
||||||
@ -87,7 +98,20 @@ class LoginStudentSelectPresenter @Inject constructor(
|
|||||||
refreshItems()
|
refreshItems()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.launch()
|
}.launch("load_data")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadAdminMessage() {
|
||||||
|
flatResourceFlow {
|
||||||
|
getAppropriateAdminMessageUseCase(
|
||||||
|
scrapperBaseUrl = registerUser.scrapperBaseUrl.orEmpty(),
|
||||||
|
type = MessageType.LOGIN_STUDENT_SELECT_MESSAGE,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.logResourceStatus("load login admin message")
|
||||||
|
.onResourceData { view?.showAdminMessage(it) }
|
||||||
|
.onResourceError { view?.showAdminMessage(null) }
|
||||||
|
.launch("load_admin_message")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getStudentsWithCurrentlyActiveSemesters(): List<LoginStudentSelectItem.Student> {
|
private fun getStudentsWithCurrentlyActiveSemesters(): List<LoginStudentSelectItem.Student> {
|
||||||
@ -108,8 +132,8 @@ class LoginStudentSelectPresenter @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun createItems(): List<LoginStudentSelectItem> = buildList {
|
private fun createItems(): List<LoginStudentSelectItem> = buildList {
|
||||||
val notEmptySymbols = registerUser.symbols.filter { it.schools.isNotEmpty() }
|
val notEmptySymbols = registerUser.symbols.filter { it.shouldShowOnTop() }
|
||||||
val emptySymbols = registerUser.symbols.filter { it.schools.isEmpty() }
|
val emptySymbols = registerUser.symbols.filter { !it.shouldShowOnTop() }
|
||||||
|
|
||||||
if (emptySymbols.isNotEmpty() && notEmptySymbols.isNotEmpty() && emptySymbols.any { it.symbol == loginData.userEnteredSymbol }) {
|
if (emptySymbols.isNotEmpty() && notEmptySymbols.isNotEmpty() && emptySymbols.any { it.symbol == loginData.userEnteredSymbol }) {
|
||||||
add(createEmptySymbolItem(emptySymbols.first { it.symbol == loginData.userEnteredSymbol }))
|
add(createEmptySymbolItem(emptySymbols.first { it.symbol == loginData.userEnteredSymbol }))
|
||||||
@ -127,6 +151,10 @@ class LoginStudentSelectPresenter @Inject constructor(
|
|||||||
add(helpItem)
|
add(helpItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun RegisterSymbol.shouldShowOnTop(): Boolean {
|
||||||
|
return schools.isNotEmpty() || error is StudentGraduateException
|
||||||
|
}
|
||||||
|
|
||||||
private fun createNotEmptySymbolItems(
|
private fun createNotEmptySymbolItems(
|
||||||
notEmptySymbols: List<RegisterSymbol>,
|
notEmptySymbols: List<RegisterSymbol>,
|
||||||
students: List<StudentWithSemesters>,
|
students: List<StudentWithSemesters>,
|
||||||
@ -336,4 +364,14 @@ class LoginStudentSelectPresenter @Inject constructor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun onAdminMessageSelected(url: String?) {
|
||||||
|
url?.let { view?.openInternetBrowser(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onAdminMessageDismissed(adminMessage: AdminMessage) {
|
||||||
|
preferencesRepository.dismissedAdminMessageIds += adminMessage.id
|
||||||
|
|
||||||
|
view?.showAdminMessage(null)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package io.github.wulkanowy.ui.modules.login.studentselect
|
package io.github.wulkanowy.ui.modules.login.studentselect
|
||||||
|
|
||||||
|
import io.github.wulkanowy.data.db.entities.AdminMessage
|
||||||
import io.github.wulkanowy.ui.base.BaseView
|
import io.github.wulkanowy.ui.base.BaseView
|
||||||
import io.github.wulkanowy.ui.modules.login.LoginData
|
import io.github.wulkanowy.ui.modules.login.LoginData
|
||||||
import io.github.wulkanowy.ui.modules.login.support.LoginSupportInfo
|
import io.github.wulkanowy.ui.modules.login.support.LoginSupportInfo
|
||||||
@ -25,4 +26,8 @@ interface LoginStudentSelectView : BaseView {
|
|||||||
fun openDiscordInvite()
|
fun openDiscordInvite()
|
||||||
|
|
||||||
fun openEmail(supportInfo: LoginSupportInfo)
|
fun openEmail(supportInfo: LoginSupportInfo)
|
||||||
|
|
||||||
|
fun showAdminMessage(adminMessage: AdminMessage?)
|
||||||
|
|
||||||
|
fun openInternetBrowser(url: String)
|
||||||
}
|
}
|
||||||
|
@ -9,13 +9,16 @@ import android.view.inputmethod.EditorInfo.IME_NULL
|
|||||||
import android.widget.ArrayAdapter
|
import android.widget.ArrayAdapter
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
import androidx.core.text.parseAsHtml
|
import androidx.core.text.parseAsHtml
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.widget.doOnTextChanged
|
import androidx.core.widget.doOnTextChanged
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import io.github.wulkanowy.R
|
import io.github.wulkanowy.R
|
||||||
|
import io.github.wulkanowy.data.db.entities.AdminMessage
|
||||||
import io.github.wulkanowy.data.pojos.RegisterUser
|
import io.github.wulkanowy.data.pojos.RegisterUser
|
||||||
import io.github.wulkanowy.data.repositories.PreferencesRepository
|
import io.github.wulkanowy.data.repositories.PreferencesRepository
|
||||||
import io.github.wulkanowy.databinding.FragmentLoginSymbolBinding
|
import io.github.wulkanowy.databinding.FragmentLoginSymbolBinding
|
||||||
import io.github.wulkanowy.ui.base.BaseFragment
|
import io.github.wulkanowy.ui.base.BaseFragment
|
||||||
|
import io.github.wulkanowy.ui.modules.dashboard.viewholders.AdminMessageViewHolder
|
||||||
import io.github.wulkanowy.ui.modules.login.LoginActivity
|
import io.github.wulkanowy.ui.modules.login.LoginActivity
|
||||||
import io.github.wulkanowy.ui.modules.login.LoginData
|
import io.github.wulkanowy.ui.modules.login.LoginData
|
||||||
import io.github.wulkanowy.ui.modules.login.support.LoginSupportDialog
|
import io.github.wulkanowy.ui.modules.login.support.LoginSupportDialog
|
||||||
@ -179,4 +182,17 @@ class LoginSymbolFragment :
|
|||||||
override fun openSupportDialog(supportInfo: LoginSupportInfo) {
|
override fun openSupportDialog(supportInfo: LoginSupportInfo) {
|
||||||
LoginSupportDialog.newInstance(supportInfo).show(childFragmentManager, "support_dialog")
|
LoginSupportDialog.newInstance(supportInfo).show(childFragmentManager, "support_dialog")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun showAdminMessage(adminMessage: AdminMessage?) {
|
||||||
|
AdminMessageViewHolder(
|
||||||
|
binding = binding.loginSymbolAdminMessage,
|
||||||
|
onAdminMessageDismissClickListener = presenter::onAdminMessageDismissed,
|
||||||
|
onAdminMessageClickListener = presenter::onAdminMessageSelected,
|
||||||
|
).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.Resource
|
||||||
import io.github.wulkanowy.data.dataOrNull
|
import io.github.wulkanowy.data.dataOrNull
|
||||||
|
import io.github.wulkanowy.data.db.entities.AdminMessage
|
||||||
|
import io.github.wulkanowy.data.enums.MessageType
|
||||||
|
import io.github.wulkanowy.data.flatResourceFlow
|
||||||
|
import io.github.wulkanowy.data.logResourceStatus
|
||||||
|
import io.github.wulkanowy.data.onResourceData
|
||||||
|
import io.github.wulkanowy.data.onResourceError
|
||||||
import io.github.wulkanowy.data.onResourceNotLoading
|
import io.github.wulkanowy.data.onResourceNotLoading
|
||||||
import io.github.wulkanowy.data.pojos.RegisterUser
|
import io.github.wulkanowy.data.pojos.RegisterUser
|
||||||
|
import io.github.wulkanowy.data.repositories.PreferencesRepository
|
||||||
import io.github.wulkanowy.data.repositories.StudentRepository
|
import io.github.wulkanowy.data.repositories.StudentRepository
|
||||||
import io.github.wulkanowy.data.resourceFlow
|
import io.github.wulkanowy.data.resourceFlow
|
||||||
|
import io.github.wulkanowy.domain.adminmessage.GetAppropriateAdminMessageUseCase
|
||||||
import io.github.wulkanowy.sdk.scrapper.getNormalizedSymbol
|
import io.github.wulkanowy.sdk.scrapper.getNormalizedSymbol
|
||||||
import io.github.wulkanowy.sdk.scrapper.login.InvalidSymbolException
|
import io.github.wulkanowy.sdk.scrapper.login.InvalidSymbolException
|
||||||
import io.github.wulkanowy.ui.base.BasePresenter
|
import io.github.wulkanowy.ui.base.BasePresenter
|
||||||
@ -21,7 +29,9 @@ import javax.inject.Inject
|
|||||||
class LoginSymbolPresenter @Inject constructor(
|
class LoginSymbolPresenter @Inject constructor(
|
||||||
studentRepository: StudentRepository,
|
studentRepository: StudentRepository,
|
||||||
private val loginErrorHandler: LoginErrorHandler,
|
private val loginErrorHandler: LoginErrorHandler,
|
||||||
private val analytics: AnalyticsHelper
|
private val analytics: AnalyticsHelper,
|
||||||
|
private val preferencesRepository: PreferencesRepository,
|
||||||
|
private val getAppropriateAdminMessageUseCase: GetAppropriateAdminMessageUseCase,
|
||||||
) : BasePresenter<LoginSymbolView>(loginErrorHandler, studentRepository) {
|
) : BasePresenter<LoginSymbolView>(loginErrorHandler, studentRepository) {
|
||||||
|
|
||||||
private var lastError: Throwable? = null
|
private var lastError: Throwable? = null
|
||||||
@ -43,6 +53,21 @@ class LoginSymbolPresenter @Inject constructor(
|
|||||||
clearAndFocusSymbol()
|
clearAndFocusSymbol()
|
||||||
showSoftKeyboard()
|
showSoftKeyboard()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loadAdminMessage()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadAdminMessage() {
|
||||||
|
flatResourceFlow {
|
||||||
|
getAppropriateAdminMessageUseCase(
|
||||||
|
scrapperBaseUrl = loginData.baseUrl,
|
||||||
|
type = MessageType.LOGIN_SYMBOL_MESSAGE,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.logResourceStatus("load login admin message")
|
||||||
|
.onResourceData { view?.showAdminMessage(it) }
|
||||||
|
.onResourceError { view?.showAdminMessage(null) }
|
||||||
|
.launch("load_admin_message")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onSymbolTextChanged() {
|
fun onSymbolTextChanged() {
|
||||||
@ -166,4 +191,14 @@ class LoginSymbolPresenter @Inject constructor(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun onAdminMessageSelected(url: String?) {
|
||||||
|
url?.let { view?.openInternetBrowser(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onAdminMessageDismissed(adminMessage: AdminMessage) {
|
||||||
|
preferencesRepository.dismissedAdminMessageIds += adminMessage.id
|
||||||
|
|
||||||
|
view?.showAdminMessage(null)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package io.github.wulkanowy.ui.modules.login.symbol
|
package io.github.wulkanowy.ui.modules.login.symbol
|
||||||
|
|
||||||
|
import io.github.wulkanowy.data.db.entities.AdminMessage
|
||||||
import io.github.wulkanowy.data.pojos.RegisterUser
|
import io.github.wulkanowy.data.pojos.RegisterUser
|
||||||
import io.github.wulkanowy.ui.base.BaseView
|
import io.github.wulkanowy.ui.base.BaseView
|
||||||
import io.github.wulkanowy.ui.modules.login.LoginData
|
import io.github.wulkanowy.ui.modules.login.LoginData
|
||||||
@ -44,4 +45,8 @@ interface LoginSymbolView : BaseView {
|
|||||||
fun openFaqPage()
|
fun openFaqPage()
|
||||||
|
|
||||||
fun openSupportDialog(supportInfo: LoginSupportInfo)
|
fun openSupportDialog(supportInfo: LoginSupportInfo)
|
||||||
|
|
||||||
|
fun showAdminMessage(adminMessage: AdminMessage?)
|
||||||
|
|
||||||
|
fun openInternetBrowser(url: String)
|
||||||
}
|
}
|
||||||
|
@ -33,4 +33,4 @@ class LuckyNumberHistoryAdapter @Inject constructor() :
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ItemViewHolder(val binding: ItemLuckyNumberHistoryBinding) : RecyclerView.ViewHolder(binding.root)
|
class ItemViewHolder(val binding: ItemLuckyNumberHistoryBinding) : RecyclerView.ViewHolder(binding.root)
|
||||||
}
|
}
|
||||||
|
@ -145,7 +145,11 @@ class LuckyNumberWidgetProvider : AppWidgetProvider() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (currentStudent != null) {
|
if (currentStudent != null) {
|
||||||
luckyNumberRepository.getLuckyNumber(currentStudent, forceRefresh = false)
|
luckyNumberRepository.getLuckyNumber(
|
||||||
|
student = currentStudent,
|
||||||
|
forceRefresh = false,
|
||||||
|
isFromAppWidget = true
|
||||||
|
)
|
||||||
.toFirstResult()
|
.toFirstResult()
|
||||||
.dataOrThrow
|
.dataOrThrow
|
||||||
} else null
|
} else null
|
||||||
|
@ -3,7 +3,9 @@ package io.github.wulkanowy.ui.modules.settings.appearance
|
|||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import androidx.core.os.bundleOf
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
|
import androidx.preference.SeekBarPreference
|
||||||
import com.yariksoffice.lingver.Lingver
|
import com.yariksoffice.lingver.Lingver
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import io.github.wulkanowy.R
|
import io.github.wulkanowy.R
|
||||||
@ -29,13 +31,31 @@ class AppearanceFragment : PreferenceFragmentCompat(),
|
|||||||
|
|
||||||
override val titleStringId get() = R.string.pref_settings_appearance_title
|
override val titleStringId get() = R.string.pref_settings_appearance_title
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun withFocusedPreference(key: String) = AppearanceFragment().apply {
|
||||||
|
arguments = bundleOf(FOCUSED_KEY to key)
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val FOCUSED_KEY = "focusedKey"
|
||||||
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
presenter.onAttachView(this)
|
presenter.onAttachView(this)
|
||||||
|
arguments?.getString(FOCUSED_KEY)?.let { scrollToPreference(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
setPreferencesFromResource(R.xml.scheme_preferences_appearance, rootKey)
|
setPreferencesFromResource(R.xml.scheme_preferences_appearance, rootKey)
|
||||||
|
val attendanceTargetPref =
|
||||||
|
findPreference<SeekBarPreference>(requireContext().getString(R.string.pref_key_attendance_target))!!
|
||||||
|
attendanceTargetPref.setOnPreferenceChangeListener { _, newValueObj ->
|
||||||
|
val newValue = (((newValueObj as Int).toDouble() + 2.5) / 5).toInt() * 5
|
||||||
|
attendanceTargetPref.value =
|
||||||
|
newValue.coerceIn(attendanceTargetPref.min, attendanceTargetPref.max)
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
|
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
|
||||||
|
@ -7,20 +7,21 @@ import android.view.ViewGroup
|
|||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.ListAdapter
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import io.github.wulkanowy.R
|
import io.github.wulkanowy.R
|
||||||
import io.github.wulkanowy.data.db.entities.Timetable
|
import io.github.wulkanowy.data.db.entities.Timetable
|
||||||
import io.github.wulkanowy.databinding.ItemTimetableBinding
|
import io.github.wulkanowy.databinding.ItemTimetableBinding
|
||||||
import io.github.wulkanowy.databinding.ItemTimetableEmptyBinding
|
import io.github.wulkanowy.databinding.ItemTimetableEmptyBinding
|
||||||
|
import io.github.wulkanowy.databinding.ItemTimetableMainAdditionalBinding
|
||||||
import io.github.wulkanowy.databinding.ItemTimetableSmallBinding
|
import io.github.wulkanowy.databinding.ItemTimetableSmallBinding
|
||||||
|
import io.github.wulkanowy.utils.SyncListAdapter
|
||||||
import io.github.wulkanowy.utils.getPlural
|
import io.github.wulkanowy.utils.getPlural
|
||||||
import io.github.wulkanowy.utils.getThemeAttrColor
|
import io.github.wulkanowy.utils.getThemeAttrColor
|
||||||
import io.github.wulkanowy.utils.toFormattedString
|
import io.github.wulkanowy.utils.toFormattedString
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class TimetableAdapter @Inject constructor() :
|
class TimetableAdapter @Inject constructor() :
|
||||||
ListAdapter<TimetableItem, RecyclerView.ViewHolder>(differ) {
|
SyncListAdapter<TimetableItem, RecyclerView.ViewHolder>(Differ) {
|
||||||
|
|
||||||
override fun getItemViewType(position: Int): Int = getItem(position).type.ordinal
|
override fun getItemViewType(position: Int): Int = getItem(position).type.ordinal
|
||||||
|
|
||||||
@ -39,6 +40,10 @@ class TimetableAdapter @Inject constructor() :
|
|||||||
TimetableItemType.EMPTY -> EmptyViewHolder(
|
TimetableItemType.EMPTY -> EmptyViewHolder(
|
||||||
ItemTimetableEmptyBinding.inflate(inflater, parent, false)
|
ItemTimetableEmptyBinding.inflate(inflater, parent, false)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
TimetableItemType.ADDITIONAL -> AdditionalViewHolder(
|
||||||
|
ItemTimetableMainAdditionalBinding.inflate(inflater, parent, false)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,16 +66,30 @@ class TimetableAdapter @Inject constructor() :
|
|||||||
binding = holder.binding,
|
binding = holder.binding,
|
||||||
item = getItem(position) as TimetableItem.Small,
|
item = getItem(position) as TimetableItem.Small,
|
||||||
)
|
)
|
||||||
|
|
||||||
is NormalViewHolder -> bindNormalView(
|
is NormalViewHolder -> bindNormalView(
|
||||||
binding = holder.binding,
|
binding = holder.binding,
|
||||||
item = getItem(position) as TimetableItem.Normal,
|
item = getItem(position) as TimetableItem.Normal,
|
||||||
)
|
)
|
||||||
|
|
||||||
is EmptyViewHolder -> bindEmptyView(
|
is EmptyViewHolder -> bindEmptyView(
|
||||||
binding = holder.binding,
|
binding = holder.binding,
|
||||||
item = getItem(position) as TimetableItem.Empty,
|
item = getItem(position) as TimetableItem.Empty,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
is AdditionalViewHolder -> bindAdditionalView(
|
||||||
|
binding = holder.binding,
|
||||||
|
item = getItem(position) as TimetableItem.Additional,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bindAdditionalView(
|
||||||
|
binding: ItemTimetableMainAdditionalBinding,
|
||||||
|
item: TimetableItem.Additional
|
||||||
|
) {
|
||||||
|
with(binding) {
|
||||||
|
timetableItemSubject.text = item.additional.subject
|
||||||
|
timetableItemTimeStart.text = item.additional.start.toFormattedString("HH:mm")
|
||||||
|
timetableItemTimeFinish.text = item.additional.end.toFormattedString("HH:mm")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -307,31 +326,32 @@ class TimetableAdapter @Inject constructor() :
|
|||||||
private class EmptyViewHolder(val binding: ItemTimetableEmptyBinding) :
|
private class EmptyViewHolder(val binding: ItemTimetableEmptyBinding) :
|
||||||
RecyclerView.ViewHolder(binding.root)
|
RecyclerView.ViewHolder(binding.root)
|
||||||
|
|
||||||
companion object {
|
private class AdditionalViewHolder(val binding: ItemTimetableMainAdditionalBinding) :
|
||||||
private val differ = object : DiffUtil.ItemCallback<TimetableItem>() {
|
RecyclerView.ViewHolder(binding.root)
|
||||||
override fun areItemsTheSame(oldItem: TimetableItem, newItem: TimetableItem): Boolean =
|
|
||||||
when {
|
|
||||||
oldItem is TimetableItem.Small && newItem is TimetableItem.Small -> {
|
|
||||||
oldItem.lesson.start == newItem.lesson.start
|
|
||||||
}
|
|
||||||
|
|
||||||
oldItem is TimetableItem.Normal && newItem is TimetableItem.Normal -> {
|
private object Differ : DiffUtil.ItemCallback<TimetableItem>() {
|
||||||
oldItem.lesson.start == newItem.lesson.start
|
override fun areItemsTheSame(oldItem: TimetableItem, newItem: TimetableItem): Boolean =
|
||||||
}
|
when {
|
||||||
|
oldItem is TimetableItem.Small && newItem is TimetableItem.Small -> {
|
||||||
else -> oldItem == newItem
|
oldItem.lesson.start == newItem.lesson.start
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun areContentsTheSame(oldItem: TimetableItem, newItem: TimetableItem) =
|
oldItem is TimetableItem.Normal && newItem is TimetableItem.Normal -> {
|
||||||
oldItem == newItem
|
oldItem.lesson.start == newItem.lesson.start
|
||||||
|
}
|
||||||
|
|
||||||
override fun getChangePayload(oldItem: TimetableItem, newItem: TimetableItem): Any? {
|
else -> oldItem == newItem
|
||||||
return if (oldItem is TimetableItem.Normal && newItem is TimetableItem.Normal) {
|
|
||||||
if (oldItem.lesson == newItem.lesson && oldItem.showGroupsInPlan == newItem.showGroupsInPlan && oldItem.timeLeft != newItem.timeLeft) {
|
|
||||||
"time_left"
|
|
||||||
} else super.getChangePayload(oldItem, newItem)
|
|
||||||
} else super.getChangePayload(oldItem, newItem)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItem: TimetableItem, newItem: TimetableItem) =
|
||||||
|
oldItem == newItem
|
||||||
|
|
||||||
|
override fun getChangePayload(oldItem: TimetableItem, newItem: TimetableItem): Any? {
|
||||||
|
return if (oldItem is TimetableItem.Normal && newItem is TimetableItem.Normal) {
|
||||||
|
if (oldItem.lesson == newItem.lesson && oldItem.showGroupsInPlan == newItem.showGroupsInPlan && oldItem.timeLeft != newItem.timeLeft) {
|
||||||
|
"time_left"
|
||||||
|
} else super.getChangePayload(oldItem, newItem)
|
||||||
|
} else super.getChangePayload(oldItem, newItem)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,11 @@ import io.github.wulkanowy.ui.modules.main.MainView
|
|||||||
import io.github.wulkanowy.ui.modules.timetable.additional.AdditionalLessonsFragment
|
import io.github.wulkanowy.ui.modules.timetable.additional.AdditionalLessonsFragment
|
||||||
import io.github.wulkanowy.ui.modules.timetable.completed.CompletedLessonsFragment
|
import io.github.wulkanowy.ui.modules.timetable.completed.CompletedLessonsFragment
|
||||||
import io.github.wulkanowy.ui.widgets.DividerItemDecoration
|
import io.github.wulkanowy.ui.widgets.DividerItemDecoration
|
||||||
import io.github.wulkanowy.utils.*
|
import io.github.wulkanowy.utils.dpToPx
|
||||||
|
import io.github.wulkanowy.utils.firstSchoolDayInSchoolYear
|
||||||
|
import io.github.wulkanowy.utils.getThemeAttrColor
|
||||||
|
import io.github.wulkanowy.utils.lastSchoolDayInSchoolYear
|
||||||
|
import io.github.wulkanowy.utils.openMaterialDatePicker
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@ -104,8 +108,11 @@ class TimetableFragment : BaseFragment<FragmentTimetableBinding>(R.layout.fragme
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun updateData(data: List<TimetableItem>) {
|
override fun updateData(data: List<TimetableItem>, isDayChanged: Boolean) {
|
||||||
timetableAdapter.submitList(data)
|
when {
|
||||||
|
isDayChanged -> timetableAdapter.recreate(data)
|
||||||
|
else -> timetableAdapter.submitList(data)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun clearData() {
|
override fun clearData() {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package io.github.wulkanowy.ui.modules.timetable
|
package io.github.wulkanowy.ui.modules.timetable
|
||||||
|
|
||||||
import io.github.wulkanowy.data.db.entities.Timetable
|
import io.github.wulkanowy.data.db.entities.Timetable
|
||||||
|
import io.github.wulkanowy.data.db.entities.TimetableAdditional
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
|
|
||||||
sealed class TimetableItem(val type: TimetableItemType) {
|
sealed class TimetableItem(val type: TimetableItemType) {
|
||||||
@ -23,6 +24,10 @@ sealed class TimetableItem(val type: TimetableItemType) {
|
|||||||
val numFrom: Int,
|
val numFrom: Int,
|
||||||
val numTo: Int
|
val numTo: Int
|
||||||
) : TimetableItem(TimetableItemType.EMPTY)
|
) : TimetableItem(TimetableItemType.EMPTY)
|
||||||
|
|
||||||
|
data class Additional(
|
||||||
|
val additional: TimetableAdditional,
|
||||||
|
) : TimetableItem(TimetableItemType.ADDITIONAL)
|
||||||
}
|
}
|
||||||
|
|
||||||
data class TimeLeft(
|
data class TimeLeft(
|
||||||
@ -34,5 +39,6 @@ data class TimeLeft(
|
|||||||
enum class TimetableItemType {
|
enum class TimetableItemType {
|
||||||
SMALL,
|
SMALL,
|
||||||
NORMAL,
|
NORMAL,
|
||||||
EMPTY
|
EMPTY,
|
||||||
|
ADDITIONAL,
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,9 @@ import android.os.Handler
|
|||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import io.github.wulkanowy.data.db.entities.Semester
|
import io.github.wulkanowy.data.db.entities.Semester
|
||||||
import io.github.wulkanowy.data.db.entities.Timetable
|
import io.github.wulkanowy.data.db.entities.Timetable
|
||||||
|
import io.github.wulkanowy.data.db.entities.TimetableAdditional
|
||||||
|
import io.github.wulkanowy.data.enums.ShowAdditionalLessonsMode.BELOW
|
||||||
|
import io.github.wulkanowy.data.enums.ShowAdditionalLessonsMode.NONE
|
||||||
import io.github.wulkanowy.data.enums.TimetableGapsMode.BETWEEN_AND_BEFORE_LESSONS
|
import io.github.wulkanowy.data.enums.TimetableGapsMode.BETWEEN_AND_BEFORE_LESSONS
|
||||||
import io.github.wulkanowy.data.enums.TimetableGapsMode.NO_GAPS
|
import io.github.wulkanowy.data.enums.TimetableGapsMode.NO_GAPS
|
||||||
import io.github.wulkanowy.data.enums.TimetableMode
|
import io.github.wulkanowy.data.enums.TimetableMode
|
||||||
@ -14,6 +17,7 @@ import io.github.wulkanowy.data.onResourceError
|
|||||||
import io.github.wulkanowy.data.onResourceIntermediate
|
import io.github.wulkanowy.data.onResourceIntermediate
|
||||||
import io.github.wulkanowy.data.onResourceNotLoading
|
import io.github.wulkanowy.data.onResourceNotLoading
|
||||||
import io.github.wulkanowy.data.onResourceSuccess
|
import io.github.wulkanowy.data.onResourceSuccess
|
||||||
|
import io.github.wulkanowy.data.pojos.TimetableFull
|
||||||
import io.github.wulkanowy.data.repositories.PreferencesRepository
|
import io.github.wulkanowy.data.repositories.PreferencesRepository
|
||||||
import io.github.wulkanowy.data.repositories.SemesterRepository
|
import io.github.wulkanowy.data.repositories.SemesterRepository
|
||||||
import io.github.wulkanowy.data.repositories.StudentRepository
|
import io.github.wulkanowy.data.repositories.StudentRepository
|
||||||
@ -81,7 +85,7 @@ class TimetablePresenter @Inject constructor(
|
|||||||
} else currentDate?.previousSchoolDay
|
} else currentDate?.previousSchoolDay
|
||||||
|
|
||||||
reloadView(date ?: return)
|
reloadView(date ?: return)
|
||||||
loadData()
|
loadData(isDayChanged = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onNextDay() {
|
fun onNextDay() {
|
||||||
@ -90,7 +94,7 @@ class TimetablePresenter @Inject constructor(
|
|||||||
} else currentDate?.nextSchoolDay
|
} else currentDate?.nextSchoolDay
|
||||||
|
|
||||||
reloadView(date ?: return)
|
reloadView(date ?: return)
|
||||||
loadData()
|
loadData(isDayChanged = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onPickDate() {
|
fun onPickDate() {
|
||||||
@ -104,7 +108,7 @@ class TimetablePresenter @Inject constructor(
|
|||||||
|
|
||||||
fun onSwipeRefresh() {
|
fun onSwipeRefresh() {
|
||||||
Timber.i("Force refreshing the timetable")
|
Timber.i("Force refreshing the timetable")
|
||||||
loadData(true)
|
loadData(forceRefresh = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onRetry() {
|
fun onRetry() {
|
||||||
@ -112,7 +116,7 @@ class TimetablePresenter @Inject constructor(
|
|||||||
showErrorView(false)
|
showErrorView(false)
|
||||||
showProgress(true)
|
showProgress(true)
|
||||||
}
|
}
|
||||||
loadData(true)
|
loadData(forceRefresh = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onDetailsClick() {
|
fun onDetailsClick() {
|
||||||
@ -145,7 +149,7 @@ class TimetablePresenter @Inject constructor(
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadData(forceRefresh: Boolean = false) {
|
private fun loadData(forceRefresh: Boolean = false, isDayChanged: Boolean = false) {
|
||||||
flatResourceFlow {
|
flatResourceFlow {
|
||||||
val student = studentRepository.getCurrentStudent()
|
val student = studentRepository.getCurrentStudent()
|
||||||
val semester = semesterRepository.getCurrentSemester(student)
|
val semester = semesterRepository.getCurrentSemester(student)
|
||||||
@ -169,9 +173,9 @@ class TimetablePresenter @Inject constructor(
|
|||||||
enableSwipe(true)
|
enableSwipe(true)
|
||||||
showProgress(false)
|
showProgress(false)
|
||||||
showErrorView(false)
|
showErrorView(false)
|
||||||
showContent(it.lessons.isNotEmpty())
|
updateData(it, isDayChanged)
|
||||||
showEmpty(it.lessons.isEmpty())
|
showContent(it.lessons.isNotEmpty() || it.additional.isNotEmpty())
|
||||||
updateData(it.lessons)
|
showEmpty(it.lessons.isEmpty() && it.additional.isEmpty())
|
||||||
setDayHeaderMessage(it.headers.find { header -> header.date == currentDate }?.content)
|
setDayHeaderMessage(it.headers.find { header -> header.date == currentDate }?.content)
|
||||||
reloadNavigation()
|
reloadNavigation()
|
||||||
}
|
}
|
||||||
@ -216,67 +220,97 @@ class TimetablePresenter @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateData(lessons: List<Timetable>) {
|
private fun updateData(lessons: TimetableFull, isDayChanged: Boolean) {
|
||||||
tickTimer?.cancel()
|
tickTimer?.cancel()
|
||||||
|
|
||||||
if (currentDate != now()) {
|
view?.updateData(createItems(lessons), isDayChanged)
|
||||||
view?.updateData(createItems(lessons))
|
if (currentDate == now()) {
|
||||||
} else {
|
tickTimer = timer(period = 2_000, initialDelay = 2_000) {
|
||||||
tickTimer = timer(period = 2_000) {
|
|
||||||
Handler(Looper.getMainLooper()).post {
|
Handler(Looper.getMainLooper()).post {
|
||||||
view?.updateData(createItems(lessons))
|
view?.updateData(createItems(lessons), isDayChanged)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createItems(items: List<Timetable>): List<TimetableItem> {
|
private sealed class Item(
|
||||||
val filteredItems = items
|
val isStudentPlan: Boolean,
|
||||||
.filter {
|
val start: Instant,
|
||||||
if (prefRepository.showWholeClassPlan == TimetableMode.ONLY_CURRENT_GROUP) {
|
val number: Int?,
|
||||||
it.isStudentPlan
|
) {
|
||||||
} else true
|
class Lesson(val lesson: Timetable) :
|
||||||
}
|
Item(lesson.isStudentPlan, lesson.start, lesson.number)
|
||||||
.sortedWith(compareBy({ item -> item.start }, { item -> !item.isStudentPlan }))
|
|
||||||
|
class Additional(val additional: TimetableAdditional) : Item(true, additional.start, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createItems(fullTimetable: TimetableFull): List<TimetableItem> {
|
||||||
|
val showAdditionalLessonsInPlan = prefRepository.showAdditionalLessonsInPlan
|
||||||
|
val allItems =
|
||||||
|
fullTimetable.lessons.map(Item::Lesson) + fullTimetable.additional.map(Item::Additional)
|
||||||
|
.takeIf { showAdditionalLessonsInPlan != NONE }.orEmpty()
|
||||||
|
|
||||||
|
val filteredItems = allItems.filter {
|
||||||
|
if (prefRepository.showWholeClassPlan == TimetableMode.ONLY_CURRENT_GROUP) {
|
||||||
|
it.isStudentPlan
|
||||||
|
} else true
|
||||||
|
}.sortedWith(
|
||||||
|
(compareBy<Item> { it is Item.Additional }
|
||||||
|
.takeIf { showAdditionalLessonsInPlan == BELOW } ?: EmptyComparator())
|
||||||
|
.thenBy { it.start }
|
||||||
|
.thenBy { !it.isStudentPlan }
|
||||||
|
)
|
||||||
|
|
||||||
var prevNum = when (prefRepository.showTimetableGaps) {
|
var prevNum = when (prefRepository.showTimetableGaps) {
|
||||||
BETWEEN_AND_BEFORE_LESSONS -> 0
|
BETWEEN_AND_BEFORE_LESSONS -> 0
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
var prevIsAdditional = false
|
||||||
return buildList {
|
return buildList {
|
||||||
filteredItems.forEachIndexed { i, it ->
|
filteredItems.forEachIndexed { i, it ->
|
||||||
if (prefRepository.showTimetableGaps != NO_GAPS && prevNum != null && it.number > prevNum!! + 1) {
|
if (prefRepository.showTimetableGaps != NO_GAPS) {
|
||||||
val emptyLesson = TimetableItem.Empty(
|
if (prevNum != null && it.number != null && it.number > prevNum!! + 1) {
|
||||||
numFrom = prevNum!! + 1,
|
if (!prevIsAdditional) {
|
||||||
numTo = it.number - 1
|
// Additional lessons do count as a lesson so don't add empty lessons
|
||||||
)
|
// when there is an additional lesson present
|
||||||
add(emptyLesson)
|
val emptyLesson = TimetableItem.Empty(
|
||||||
|
numFrom = prevNum!! + 1, numTo = it.number - 1
|
||||||
|
)
|
||||||
|
add(emptyLesson)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
prevNum = it.number
|
||||||
|
prevIsAdditional = it is Item.Additional
|
||||||
}
|
}
|
||||||
|
|
||||||
if (it.isStudentPlan) {
|
if (it is Item.Lesson) {
|
||||||
val normalLesson = TimetableItem.Normal(
|
if (it.isStudentPlan) {
|
||||||
lesson = it,
|
val normalLesson = TimetableItem.Normal(
|
||||||
showGroupsInPlan = prefRepository.showGroupsInPlan,
|
lesson = it.lesson,
|
||||||
timeLeft = filteredItems.getTimeLeftForLesson(it, i),
|
showGroupsInPlan = prefRepository.showGroupsInPlan,
|
||||||
onClick = ::onTimetableItemSelected,
|
timeLeft = filteredItems.getTimeLeftForLesson(it.lesson, i),
|
||||||
isLessonNumberVisible = !isEduOne
|
onClick = ::onTimetableItemSelected,
|
||||||
)
|
isLessonNumberVisible = !isEduOne
|
||||||
add(normalLesson)
|
)
|
||||||
} else {
|
add(normalLesson)
|
||||||
val smallLesson = TimetableItem.Small(
|
} else {
|
||||||
lesson = it,
|
val smallLesson = TimetableItem.Small(
|
||||||
onClick = ::onTimetableItemSelected,
|
lesson = it.lesson,
|
||||||
isLessonNumberVisible = !isEduOne
|
onClick = ::onTimetableItemSelected,
|
||||||
)
|
isLessonNumberVisible = !isEduOne
|
||||||
add(smallLesson)
|
)
|
||||||
|
add(smallLesson)
|
||||||
|
}
|
||||||
|
} else if (it is Item.Additional) {
|
||||||
|
// If the user disabled showing additional lessons, they would've been filtered
|
||||||
|
// out already, so there's no need to check it again.
|
||||||
|
add(TimetableItem.Additional(it.additional))
|
||||||
}
|
}
|
||||||
|
|
||||||
prevNum = it.number
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun List<Timetable>.getTimeLeftForLesson(lesson: Timetable, index: Int): TimeLeft {
|
private fun List<Item>.getTimeLeftForLesson(lesson: Timetable, index: Int): TimeLeft {
|
||||||
val isShowTimeUntil = lesson.isShowTimeUntil(getPreviousLesson(index))
|
val isShowTimeUntil = lesson.isShowTimeUntil(getPreviousLesson(index))
|
||||||
return TimeLeft(
|
return TimeLeft(
|
||||||
until = lesson.until.plusMinutes(1).takeIf { isShowTimeUntil },
|
until = lesson.until.plusMinutes(1).takeIf { isShowTimeUntil },
|
||||||
@ -285,11 +319,20 @@ class TimetablePresenter @Inject constructor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun List<Timetable>.getPreviousLesson(position: Int): Instant? {
|
private fun List<Item>.getPreviousLesson(position: Int): Instant? {
|
||||||
return filter { it.isStudentPlan }
|
val lessonAdditionalOffset = filterIndexed { i, item ->
|
||||||
.getOrNull(position - 1 - filterIndexed { i, item -> i < position && !item.isStudentPlan }.size)
|
i < position && item is Item.Additional
|
||||||
|
}.size
|
||||||
|
val lessonStudentPlanOffset = filterIndexed { i, item ->
|
||||||
|
i < position && !item.isStudentPlan
|
||||||
|
}.size
|
||||||
|
val lessonIndex = position - 1 - lessonAdditionalOffset - lessonStudentPlanOffset
|
||||||
|
|
||||||
|
return filterIsInstance<Item.Lesson>()
|
||||||
|
.filter { it.isStudentPlan }
|
||||||
|
.getOrNull(lessonIndex)
|
||||||
?.let {
|
?.let {
|
||||||
if (!it.canceled && it.isStudentPlan) it.end
|
if (!it.lesson.canceled && it.isStudentPlan) it.lesson.end
|
||||||
else null
|
else null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -342,3 +385,7 @@ class TimetablePresenter @Inject constructor(
|
|||||||
super.onDetachView()
|
super.onDetachView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class EmptyComparator<T> : Comparator<T> {
|
||||||
|
override fun compare(o1: T, o2: T) = 0
|
||||||
|
}
|
||||||
|
@ -12,7 +12,7 @@ interface TimetableView : BaseView {
|
|||||||
|
|
||||||
fun initView()
|
fun initView()
|
||||||
|
|
||||||
fun updateData(data: List<TimetableItem>)
|
fun updateData(data: List<TimetableItem>, isDayChanged: Boolean)
|
||||||
|
|
||||||
fun updateNavigationDay(date: String)
|
fun updateNavigationDay(date: String)
|
||||||
|
|
||||||
|
@ -13,7 +13,11 @@ import io.github.wulkanowy.ui.modules.main.MainActivity
|
|||||||
import io.github.wulkanowy.ui.modules.main.MainView
|
import io.github.wulkanowy.ui.modules.main.MainView
|
||||||
import io.github.wulkanowy.ui.modules.timetable.additional.add.AdditionalLessonAddDialog
|
import io.github.wulkanowy.ui.modules.timetable.additional.add.AdditionalLessonAddDialog
|
||||||
import io.github.wulkanowy.ui.widgets.DividerItemDecoration
|
import io.github.wulkanowy.ui.widgets.DividerItemDecoration
|
||||||
import io.github.wulkanowy.utils.*
|
import io.github.wulkanowy.utils.dpToPx
|
||||||
|
import io.github.wulkanowy.utils.firstSchoolDayInSchoolYear
|
||||||
|
import io.github.wulkanowy.utils.getThemeAttrColor
|
||||||
|
import io.github.wulkanowy.utils.lastSchoolDayInSchoolYear
|
||||||
|
import io.github.wulkanowy.utils.openMaterialDatePicker
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@ -132,8 +136,12 @@ class AdditionalLessonsFragment :
|
|||||||
binding.additionalLessonsNextButton.visibility = if (show) View.VISIBLE else View.INVISIBLE
|
binding.additionalLessonsNextButton.visibility = if (show) View.VISIBLE else View.INVISIBLE
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun showAddAdditionalLessonDialog() {
|
override fun showAddAdditionalLessonDialog(currentDate: LocalDate) {
|
||||||
(activity as? MainActivity)?.showDialogFragment(AdditionalLessonAddDialog.newInstance())
|
(activity as? MainActivity)?.showDialogFragment(
|
||||||
|
AdditionalLessonAddDialog.newInstance(
|
||||||
|
currentDate
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun showDatePickerDialog(selectedDate: LocalDate) {
|
override fun showDatePickerDialog(selectedDate: LocalDate) {
|
||||||
|
@ -1,14 +1,27 @@
|
|||||||
package io.github.wulkanowy.ui.modules.timetable.additional
|
package io.github.wulkanowy.ui.modules.timetable.additional
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import io.github.wulkanowy.data.*
|
|
||||||
import io.github.wulkanowy.data.db.entities.TimetableAdditional
|
import io.github.wulkanowy.data.db.entities.TimetableAdditional
|
||||||
|
import io.github.wulkanowy.data.flatResourceFlow
|
||||||
|
import io.github.wulkanowy.data.logResourceStatus
|
||||||
|
import io.github.wulkanowy.data.onResourceData
|
||||||
|
import io.github.wulkanowy.data.onResourceError
|
||||||
|
import io.github.wulkanowy.data.onResourceNotLoading
|
||||||
|
import io.github.wulkanowy.data.onResourceSuccess
|
||||||
import io.github.wulkanowy.data.repositories.SemesterRepository
|
import io.github.wulkanowy.data.repositories.SemesterRepository
|
||||||
import io.github.wulkanowy.data.repositories.StudentRepository
|
import io.github.wulkanowy.data.repositories.StudentRepository
|
||||||
import io.github.wulkanowy.data.repositories.TimetableRepository
|
import io.github.wulkanowy.data.repositories.TimetableRepository
|
||||||
|
import io.github.wulkanowy.domain.timetable.IsStudentHasLessonsOnWeekendUseCase
|
||||||
import io.github.wulkanowy.ui.base.BasePresenter
|
import io.github.wulkanowy.ui.base.BasePresenter
|
||||||
import io.github.wulkanowy.ui.base.ErrorHandler
|
import io.github.wulkanowy.ui.base.ErrorHandler
|
||||||
import io.github.wulkanowy.utils.*
|
import io.github.wulkanowy.utils.AnalyticsHelper
|
||||||
|
import io.github.wulkanowy.utils.capitalise
|
||||||
|
import io.github.wulkanowy.utils.getLastSchoolDayIfHoliday
|
||||||
|
import io.github.wulkanowy.utils.isHolidays
|
||||||
|
import io.github.wulkanowy.utils.nextOrSameSchoolDay
|
||||||
|
import io.github.wulkanowy.utils.nextSchoolDay
|
||||||
|
import io.github.wulkanowy.utils.previousSchoolDay
|
||||||
|
import io.github.wulkanowy.utils.toFormattedString
|
||||||
import kotlinx.coroutines.flow.catch
|
import kotlinx.coroutines.flow.catch
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
@ -22,11 +35,14 @@ class AdditionalLessonsPresenter @Inject constructor(
|
|||||||
errorHandler: ErrorHandler,
|
errorHandler: ErrorHandler,
|
||||||
private val semesterRepository: SemesterRepository,
|
private val semesterRepository: SemesterRepository,
|
||||||
private val timetableRepository: TimetableRepository,
|
private val timetableRepository: TimetableRepository,
|
||||||
|
private val isStudentHasLessonsOnWeekendUseCase: IsStudentHasLessonsOnWeekendUseCase,
|
||||||
private val analytics: AnalyticsHelper
|
private val analytics: AnalyticsHelper
|
||||||
) : BasePresenter<AdditionalLessonsView>(errorHandler, studentRepository) {
|
) : BasePresenter<AdditionalLessonsView>(errorHandler, studentRepository) {
|
||||||
|
|
||||||
private var baseDate: LocalDate = LocalDate.now().nextOrSameSchoolDay
|
private var baseDate: LocalDate = LocalDate.now().nextOrSameSchoolDay
|
||||||
|
|
||||||
|
private var isWeekendHasLessons: Boolean = false
|
||||||
|
|
||||||
lateinit var currentDate: LocalDate
|
lateinit var currentDate: LocalDate
|
||||||
private set
|
private set
|
||||||
|
|
||||||
@ -43,12 +59,18 @@ class AdditionalLessonsPresenter @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun onPreviousDay() {
|
fun onPreviousDay() {
|
||||||
loadData(currentDate.previousSchoolDay)
|
val date = if (isWeekendHasLessons) {
|
||||||
|
currentDate.minusDays(1)
|
||||||
|
} else currentDate.previousSchoolDay
|
||||||
|
loadData(date)
|
||||||
reloadView()
|
reloadView()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onNextDay() {
|
fun onNextDay() {
|
||||||
loadData(currentDate.nextSchoolDay)
|
val date = if (isWeekendHasLessons) {
|
||||||
|
currentDate.plusDays(1)
|
||||||
|
} else currentDate.nextSchoolDay
|
||||||
|
loadData(date)
|
||||||
reloadView()
|
reloadView()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,7 +79,7 @@ class AdditionalLessonsPresenter @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun onAdditionalLessonAddButtonClicked() {
|
fun onAdditionalLessonAddButtonClicked() {
|
||||||
view?.showAddAdditionalLessonDialog()
|
view?.showAddAdditionalLessonDialog(currentDate)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onDateSet(year: Int, month: Int, day: Int) {
|
fun onDateSet(year: Int, month: Int, day: Int) {
|
||||||
@ -131,6 +153,8 @@ class AdditionalLessonsPresenter @Inject constructor(
|
|||||||
flatResourceFlow {
|
flatResourceFlow {
|
||||||
val student = studentRepository.getCurrentStudent()
|
val student = studentRepository.getCurrentStudent()
|
||||||
val semester = semesterRepository.getCurrentSemester(student)
|
val semester = semesterRepository.getCurrentSemester(student)
|
||||||
|
|
||||||
|
isWeekendHasLessons = isStudentHasLessonsOnWeekendUseCase(semester, currentDate)
|
||||||
timetableRepository.getTimetable(
|
timetableRepository.getTimetable(
|
||||||
student = student,
|
student = student,
|
||||||
semester = semester,
|
semester = semester,
|
||||||
|
@ -36,7 +36,7 @@ interface AdditionalLessonsView : BaseView {
|
|||||||
|
|
||||||
fun showDatePickerDialog(selectedDate: LocalDate)
|
fun showDatePickerDialog(selectedDate: LocalDate)
|
||||||
|
|
||||||
fun showAddAdditionalLessonDialog()
|
fun showAddAdditionalLessonDialog(currentDate: LocalDate)
|
||||||
|
|
||||||
fun showSuccessMessage()
|
fun showSuccessMessage()
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ package io.github.wulkanowy.ui.modules.timetable.additional.add
|
|||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import androidx.core.os.bundleOf
|
||||||
import androidx.core.widget.doOnTextChanged
|
import androidx.core.widget.doOnTextChanged
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import com.google.android.material.timepicker.MaterialTimePicker
|
import com.google.android.material.timepicker.MaterialTimePicker
|
||||||
@ -26,10 +27,12 @@ class AdditionalLessonAddDialog : BaseDialogFragment<DialogAdditionalAddBinding>
|
|||||||
lateinit var presenter: AdditionalLessonAddPresenter
|
lateinit var presenter: AdditionalLessonAddPresenter
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun newInstance() = AdditionalLessonAddDialog()
|
const val ARGUMENT_KEY = "additional_lesson_default_date"
|
||||||
|
fun newInstance(defaultDate: LocalDate) = AdditionalLessonAddDialog().apply {
|
||||||
|
arguments = bundleOf(ARGUMENT_KEY to defaultDate.toEpochDay())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
return MaterialAlertDialogBuilder(requireContext(), theme)
|
return MaterialAlertDialogBuilder(requireContext(), theme)
|
||||||
.setView(
|
.setView(
|
||||||
@ -40,10 +43,13 @@ class AdditionalLessonAddDialog : BaseDialogFragment<DialogAdditionalAddBinding>
|
|||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
arguments?.getLong(ARGUMENT_KEY)?.let(LocalDate::ofEpochDay)?.let {
|
||||||
|
presenter.onDateSelected(it)
|
||||||
|
}
|
||||||
presenter.onAttachView(this)
|
presenter.onAttachView(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun initView() {
|
override fun initView(selectedDate: LocalDate) {
|
||||||
with(binding) {
|
with(binding) {
|
||||||
additionalLessonDialogStartEdit.doOnTextChanged { _, _, _, _ ->
|
additionalLessonDialogStartEdit.doOnTextChanged { _, _, _, _ ->
|
||||||
additionalLessonDialogStart.isErrorEnabled = false
|
additionalLessonDialogStart.isErrorEnabled = false
|
||||||
@ -53,6 +59,7 @@ class AdditionalLessonAddDialog : BaseDialogFragment<DialogAdditionalAddBinding>
|
|||||||
additionalLessonDialogEnd.isErrorEnabled = false
|
additionalLessonDialogEnd.isErrorEnabled = false
|
||||||
additionalLessonDialogEnd.error = null
|
additionalLessonDialogEnd.error = null
|
||||||
}
|
}
|
||||||
|
additionalLessonDialogDateEdit.setText(selectedDate.toFormattedString())
|
||||||
additionalLessonDialogDateEdit.doOnTextChanged { _, _, _, _ ->
|
additionalLessonDialogDateEdit.doOnTextChanged { _, _, _, _ ->
|
||||||
additionalLessonDialogDate.isErrorEnabled = false
|
additionalLessonDialogDate.isErrorEnabled = false
|
||||||
additionalLessonDialogDate.error = null
|
additionalLessonDialogDate.error = null
|
||||||
@ -61,7 +68,6 @@ class AdditionalLessonAddDialog : BaseDialogFragment<DialogAdditionalAddBinding>
|
|||||||
additionalLessonDialogContent.isErrorEnabled = false
|
additionalLessonDialogContent.isErrorEnabled = false
|
||||||
additionalLessonDialogContent.error = null
|
additionalLessonDialogContent.error = null
|
||||||
}
|
}
|
||||||
|
|
||||||
additionalLessonDialogAdd.setOnClickListener {
|
additionalLessonDialogAdd.setOnClickListener {
|
||||||
presenter.onAddAdditionalClicked(
|
presenter.onAddAdditionalClicked(
|
||||||
start = additionalLessonDialogStartEdit.text?.toString(),
|
start = additionalLessonDialogStartEdit.text?.toString(),
|
||||||
@ -155,7 +161,9 @@ class AdditionalLessonAddDialog : BaseDialogFragment<DialogAdditionalAddBinding>
|
|||||||
.build()
|
.build()
|
||||||
|
|
||||||
timePicker.addOnPositiveButtonClickListener {
|
timePicker.addOnPositiveButtonClickListener {
|
||||||
onTimeSelected(LocalTime.of(timePicker.hour, timePicker.minute))
|
if (isAdded) {
|
||||||
|
onTimeSelected(LocalTime.of(timePicker.hour, timePicker.minute))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!parentFragmentManager.isStateSaved) {
|
if (!parentFragmentManager.isStateSaved) {
|
||||||
|
@ -10,9 +10,12 @@ import io.github.wulkanowy.utils.lastSchoolDayInSchoolYear
|
|||||||
import io.github.wulkanowy.utils.toLocalDate
|
import io.github.wulkanowy.utils.toLocalDate
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.time.*
|
import java.time.LocalDate
|
||||||
|
import java.time.LocalTime
|
||||||
|
import java.time.ZoneId
|
||||||
|
import java.time.ZonedDateTime
|
||||||
import java.time.temporal.ChronoUnit
|
import java.time.temporal.ChronoUnit
|
||||||
import java.util.*
|
import java.util.UUID
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class AdditionalLessonAddPresenter @Inject constructor(
|
class AdditionalLessonAddPresenter @Inject constructor(
|
||||||
@ -30,7 +33,7 @@ class AdditionalLessonAddPresenter @Inject constructor(
|
|||||||
|
|
||||||
override fun onAttachView(view: AdditionalLessonAddView) {
|
override fun onAttachView(view: AdditionalLessonAddView) {
|
||||||
super.onAttachView(view)
|
super.onAttachView(view)
|
||||||
view.initView()
|
view.initView(selectedDate)
|
||||||
Timber.i("AdditionalLesson details view was initialized")
|
Timber.i("AdditionalLesson details view was initialized")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ import java.time.LocalTime
|
|||||||
|
|
||||||
interface AdditionalLessonAddView : BaseView {
|
interface AdditionalLessonAddView : BaseView {
|
||||||
|
|
||||||
fun initView()
|
fun initView(selectedDate: LocalDate)
|
||||||
|
|
||||||
fun closeDialog()
|
fun closeDialog()
|
||||||
|
|
||||||
|
@ -101,7 +101,14 @@ class TimetableWidgetFactory(
|
|||||||
private suspend fun getLessons(
|
private suspend fun getLessons(
|
||||||
student: Student, semester: Semester, date: LocalDate
|
student: Student, semester: Semester, date: LocalDate
|
||||||
): List<Timetable> {
|
): List<Timetable> {
|
||||||
val timetable = timetableRepository.getTimetable(student, semester, date, date, false)
|
val timetable = timetableRepository.getTimetable(
|
||||||
|
student = student,
|
||||||
|
semester = semester,
|
||||||
|
start = date,
|
||||||
|
end = date,
|
||||||
|
forceRefresh = false,
|
||||||
|
isFromAppWidget = true
|
||||||
|
)
|
||||||
val lessons = timetable.toFirstResult().dataOrThrow.lessons
|
val lessons = timetable.toFirstResult().dataOrThrow.lessons
|
||||||
return lessons.sortedBy { it.start }
|
return lessons.sortedBy { it.start }
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,34 @@
|
|||||||
|
package io.github.wulkanowy.utils
|
||||||
|
|
||||||
|
import android.appwidget.AppWidgetManager
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
|
class AppWidgetUpdater @Inject constructor(
|
||||||
|
@ApplicationContext private val context: Context,
|
||||||
|
private val appWidgetManager: AppWidgetManager
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun updateAllAppWidgetsByProvider(providerClass: KClass<out BroadcastReceiver>) {
|
||||||
|
try {
|
||||||
|
val ids = appWidgetManager.getAppWidgetIds(ComponentName(context, providerClass.java))
|
||||||
|
if (ids.isEmpty()) return
|
||||||
|
|
||||||
|
val intent = Intent(context, providerClass.java)
|
||||||
|
.apply {
|
||||||
|
action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
|
||||||
|
putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids)
|
||||||
|
}
|
||||||
|
|
||||||
|
context.sendBroadcast(intent)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "Failed to update all widgets for provider $providerClass")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -10,19 +10,19 @@ import io.github.wulkanowy.sdk.scrapper.attendance.AttendanceCategory
|
|||||||
* (https://www.vulcan.edu.pl/vulcang_files/user/AABW/AABW-PDF/uonetplus/uonetplus_Frekwencja-liczby-obecnych-nieobecnych.pdf)
|
* (https://www.vulcan.edu.pl/vulcang_files/user/AABW/AABW-PDF/uonetplus/uonetplus_Frekwencja-liczby-obecnych-nieobecnych.pdf)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
private inline val AttendanceSummary.allPresences: Double
|
inline val AttendanceSummary.allPresences: Int
|
||||||
get() = presence.toDouble() + absenceForSchoolReasons + lateness + latenessExcused
|
get() = presence + absenceForSchoolReasons + lateness + latenessExcused
|
||||||
|
|
||||||
private inline val AttendanceSummary.allAbsences: Double
|
inline val AttendanceSummary.allAbsences: Int
|
||||||
get() = absence.toDouble() + absenceExcused
|
get() = absence + absenceExcused
|
||||||
|
|
||||||
inline val Attendance.isExcusableOrNotExcused: Boolean
|
inline val Attendance.isExcusableOrNotExcused: Boolean
|
||||||
get() = (excusable || ((absence || lateness) && !excused)) && excuseStatus == null
|
get() = (excusable || ((absence || lateness) && !excused)) && excuseStatus == null
|
||||||
|
|
||||||
fun AttendanceSummary.calculatePercentage() = calculatePercentage(allPresences, allAbsences)
|
fun AttendanceSummary.calculatePercentage() = calculatePercentage(allPresences.toDouble(), allAbsences.toDouble())
|
||||||
|
|
||||||
fun List<AttendanceSummary>.calculatePercentage(): Double {
|
fun List<AttendanceSummary>.calculatePercentage(): Double {
|
||||||
return calculatePercentage(sumOf { it.allPresences }, sumOf { it.allAbsences })
|
return calculatePercentage(sumOf { it.allPresences.toDouble() }, sumOf { it.allAbsences.toDouble() })
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun calculatePercentage(presence: Double, absence: Double): Double {
|
private fun calculatePercentage(presence: Double, absence: Double): Double {
|
||||||
|
@ -23,7 +23,7 @@ fun getRefreshKey(name: String, semester: Semester): String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getRefreshKey(name: String, student: Student): String {
|
fun getRefreshKey(name: String, student: Student): String {
|
||||||
return "${name}_${student.userLoginId}"
|
return "${name}_${student.studentId}"
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getRefreshKey(name: String, mailbox: Mailbox?, folder: MessageFolder): String {
|
fun getRefreshKey(name: String, mailbox: Mailbox?, folder: MessageFolder): String {
|
||||||
|
@ -18,7 +18,7 @@ fun Semester.isCurrent(now: LocalDate = now()): Boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun List<Semester>.getCurrentOrLast(): Semester {
|
fun List<Semester>.getCurrentOrLast(): Semester {
|
||||||
if (isEmpty()) throw RuntimeException("Empty semester list")
|
if (isEmpty()) throw IllegalStateException("Empty semester list")
|
||||||
|
|
||||||
// when there is only one current semester
|
// when there is only one current semester
|
||||||
singleOrNull { it.isCurrent() }?.let { return it }
|
singleOrNull { it.isCurrent() }?.let { return it }
|
||||||
|
@ -0,0 +1,66 @@
|
|||||||
|
package io.github.wulkanowy.utils
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom alternative to androidx.recyclerview.widget.ListAdapter. ListAdapter is asynchronous which
|
||||||
|
* caused data race problems in views when a Resource.Error arrived shortly after
|
||||||
|
* Resource.Intermediate/Success - occasionally in that case the user could see both the Resource's
|
||||||
|
* data and an error message one on top of the other. This is synchronized by design to avoid that
|
||||||
|
* problem, however it retains the quality of life improvements of the original.
|
||||||
|
*/
|
||||||
|
abstract class SyncListAdapter<T : Any, VH : RecyclerView.ViewHolder> private constructor(
|
||||||
|
private val updateStrategy: SyncListAdapter<T, VH>.(List<T>) -> Unit
|
||||||
|
) : RecyclerView.Adapter<VH>() {
|
||||||
|
|
||||||
|
constructor(differ: DiffUtil.ItemCallback<T>) : this({ newItems ->
|
||||||
|
val diffResult = DiffUtil.calculateDiff(toCallback(differ, items, newItems))
|
||||||
|
items = newItems
|
||||||
|
diffResult.dispatchUpdatesTo(this)
|
||||||
|
})
|
||||||
|
|
||||||
|
var items = emptyList<T>()
|
||||||
|
private set
|
||||||
|
|
||||||
|
final override fun getItemCount() = items.size
|
||||||
|
|
||||||
|
fun getItem(position: Int): T {
|
||||||
|
return items[position]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates all items, same as submitList, however also disables animations temporarily.
|
||||||
|
* This prevents a flashing effect on some views. Should be used in favor of submitList when
|
||||||
|
* all data is changed (e.g. the selected day changes in timetable causing all lessons to change).
|
||||||
|
*/
|
||||||
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
|
fun recreate(data: List<T>) {
|
||||||
|
items = data
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun submitList(data: List<T>) {
|
||||||
|
updateStrategy(data.toList())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <T : Any> toCallback(
|
||||||
|
itemCallback: DiffUtil.ItemCallback<T>,
|
||||||
|
old: List<T>,
|
||||||
|
new: List<T>,
|
||||||
|
) = object : DiffUtil.Callback() {
|
||||||
|
override fun getOldListSize() = old.size
|
||||||
|
|
||||||
|
override fun getNewListSize() = new.size
|
||||||
|
|
||||||
|
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =
|
||||||
|
itemCallback.areItemsTheSame(old[oldItemPosition], new[newItemPosition])
|
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) =
|
||||||
|
itemCallback.areContentsTheSame(old[oldItemPosition], new[newItemPosition])
|
||||||
|
|
||||||
|
override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int) =
|
||||||
|
itemCallback.getChangePayload(old[oldItemPosition], new[newItemPosition])
|
||||||
|
}
|
||||||
|
}
|
@ -6,7 +6,7 @@ Zvýrazněné vlastnosti a funkce:
|
|||||||
- šťastné číslo,
|
- šťastné číslo,
|
||||||
- náhled na další a dokončené lekce,
|
- náhled na další a dokončené lekce,
|
||||||
- tmavý motiv,
|
- tmavý motiv,
|
||||||
- žádné reklamy,
|
- volitelné reklamy,
|
||||||
- offline režim,
|
- offline režim,
|
||||||
- upozornění.
|
- upozornění.
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ Wyróżnione cechy i funkcje:
|
|||||||
- szczęśliwy numerek,
|
- szczęśliwy numerek,
|
||||||
- podgląd lekcji dodatkowych i zrealizowanych,
|
- podgląd lekcji dodatkowych i zrealizowanych,
|
||||||
- ciemny motyw.
|
- ciemny motyw.
|
||||||
- brak reklam,
|
- opcjonalne reklam,
|
||||||
- tryb offline,
|
- tryb offline,
|
||||||
- powiadomienia.
|
- powiadomienia.
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 342 KiB After Width: | Height: | Size: 184 KiB |
Before Width: | Height: | Size: 445 KiB After Width: | Height: | Size: 261 KiB |
Before Width: | Height: | Size: 363 KiB After Width: | Height: | Size: 227 KiB |
Before Width: | Height: | Size: 270 KiB After Width: | Height: | Size: 171 KiB |
Before Width: | Height: | Size: 469 KiB After Width: | Height: | Size: 251 KiB |
Before Width: | Height: | Size: 346 KiB After Width: | Height: | Size: 204 KiB |
Before Width: | Height: | Size: 293 KiB After Width: | Height: | Size: 189 KiB |
Before Width: | Height: | Size: 414 KiB After Width: | Height: | Size: 251 KiB |
@ -6,7 +6,7 @@ Zvýraznené vlastnosti a funkcie:
|
|||||||
- šťastné číslo,
|
- šťastné číslo,
|
||||||
- náhľad na ďalšie a dokončené lekcie,
|
- náhľad na ďalšie a dokončené lekcie,
|
||||||
- tmavý motív,
|
- tmavý motív,
|
||||||
- žiadne reklamy,
|
- voliteľné reklamy,
|
||||||
- offline režim,
|
- offline režim,
|
||||||
- upozornenia.
|
- upozornenia.
|
||||||
|
|
||||||
|
@ -1,8 +1,5 @@
|
|||||||
Wersja 2.5.3
|
Wersja 2.6.4
|
||||||
|
|
||||||
— naprawiliśmy wyświetlanie błędu "Brak uprawnień" po starcie aplikacji u użytkowników eduOne
|
— naprawiliśmy dostęp do modułu ucznia i modułu wiadomości po kolejnej próbie blokady nas (niestety jeszcze bez nowego modułu ucznia)
|
||||||
— naprawiliśmy obsługę autoryzacji u użytkowników eduOne
|
|
||||||
— ukryliśmy numery lekcji i oceny klasy u użytkowników eduOne, bo VULCAN te funkcje usunął
|
|
||||||
— naprawiliśmy inne rzeczy u użytkowników eduOne (jak brak opisu oceny czy ładowanie danych na kilku ekranach)
|
|
||||||
|
|
||||||
Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases
|
Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||||
|
android:viewportHeight="24" android:viewportWidth="24"
|
||||||
|
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M19,7H9C7.9,7 7,7.9 7,9v10c0,1.1 0.9,2 2,2h10c1.1,0 2,-0.9 2,-2V9C21,7.9 20.1,7 19,7zM19,9v2H9V9H19zM13,15v-2h2v2H13zM15,17v2h-2v-2H15zM11,15H9v-2h2V15zM17,13h2v2h-2V13zM9,17h2v2H9V17zM17,19v-2h2v2H17zM6,17H5c-1.1,0 -2,-0.9 -2,-2V5c0,-1.1 0.9,-2 2,-2h10c1.1,0 2,0.9 2,2v1h-2V5H5v10h1V17z"/>
|
||||||
|
</vector>
|
@ -32,12 +32,11 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginHorizontal="24dp"
|
android:layout_marginHorizontal="24dp"
|
||||||
android:layout_marginTop="8dp"
|
android:layout_marginTop="8dp"
|
||||||
android:textSize="16sp"
|
android:textSize="14sp"
|
||||||
app:layout_constraintTop_toBottomOf="@id/auth_title"
|
app:layout_constraintTop_toBottomOf="@id/auth_title"
|
||||||
app:lineHeight="24sp"
|
app:lineHeight="18sp"
|
||||||
tools:text="@string/auth_description" />
|
tools:text="@string/auth_description" />
|
||||||
|
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputLayout
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
android:id="@+id/auth_input_layout"
|
android:id="@+id/auth_input_layout"
|
||||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
|
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
|
||||||
|
103
app/src/main/res/layout/fragment_attendance_calculator.xml
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:context=".ui.modules.attendance.calculator.AttendanceCalculatorFragment">
|
||||||
|
|
||||||
|
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
|
android:id="@+id/attendanceCalculatorSwipe"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/attendanceCalculatorRecycler"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:listitem="@layout/item_attendance_calculator_header" />
|
||||||
|
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.progressindicator.CircularProgressIndicator
|
||||||
|
android:id="@+id/attendanceCalculatorProgress"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:indeterminate="true"
|
||||||
|
tools:visibility="gone" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/attendanceCalculatorEmpty"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:visibility="invisible"
|
||||||
|
tools:ignore="UseCompoundDrawables">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="100dp"
|
||||||
|
android:layout_height="100dp"
|
||||||
|
app:srcCompat="@drawable/ic_main_attendance"
|
||||||
|
app:tint="?colorOnBackground"
|
||||||
|
tools:ignore="contentDescription" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="20dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="@string/attendance_no_items"
|
||||||
|
android:textSize="20sp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/attendanceCalculatorError"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:visibility="invisible"
|
||||||
|
tools:ignore="UseCompoundDrawables"
|
||||||
|
tools:visibility="invisible">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="100dp"
|
||||||
|
android:layout_height="100dp"
|
||||||
|
app:srcCompat="@drawable/ic_error"
|
||||||
|
app:tint="?colorOnBackground"
|
||||||
|
tools:ignore="contentDescription" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/attendanceCalculatorErrorMessage"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="20dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:padding="8dp"
|
||||||
|
android:text="@string/error_unknown"
|
||||||
|
android:textSize="20sp" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/attendanceCalculatorErrorDetails"
|
||||||
|
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:layout_marginRight="8dp"
|
||||||
|
android:text="@string/all_details" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/attendanceCalculatorErrorRetry"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/all_retry" />
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
</FrameLayout>
|
@ -11,6 +11,18 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<include
|
||||||
|
android:id="@+id/login_student_select_admin_message"
|
||||||
|
layout="@layout/item_dashboard_admin_message"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="16dp"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintVertical_chainStyle="packed"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/loginStudentSelectHeader"
|
android:id="@+id/loginStudentSelectHeader"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@ -28,7 +40,7 @@
|
|||||||
app:layout_constraintBottom_toTopOf="@id/loginStudentSelectRecycler"
|
app:layout_constraintBottom_toTopOf="@id/loginStudentSelectRecycler"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toBottomOf="@id/login_student_select_admin_message"
|
||||||
app:layout_constraintVertical_chainStyle="packed" />
|
app:layout_constraintVertical_chainStyle="packed" />
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|