forked from github/wulkanowy-mirror
Compare commits
77 Commits
2.3.5
...
feature/up
Author | SHA1 | Date | |
---|---|---|---|
7a85d13812 | |||
961bc24f27 | |||
8a90b61b97 | |||
6a8f6f9496 | |||
afb5ae741c | |||
95e41b5570 | |||
eb6fdd900e | |||
88def5eff8 | |||
0e99c81eb8 | |||
38c00ddab5 | |||
c72cc39920 | |||
4ef9fb1f28 | |||
5dd5697f65 | |||
a7c2009e49 | |||
a71ef4a4b2 | |||
30413086fc | |||
98ddf97855 | |||
8f5a210ec7 | |||
ce09b07cfd | |||
0a1f7270b4 | |||
47d8513a77 | |||
00432ab911 | |||
f455064b9d | |||
2bbc157d03 | |||
a0a0b8dea6 | |||
3bab883a56 | |||
b319bb03cd | |||
333306e7ba | |||
fb240938ed | |||
dc9af29a44 | |||
e9d64de0cb | |||
05bda598fc | |||
3564366a8f | |||
f2d26453ed | |||
ccba31f2e8 | |||
a7238e3f23 | |||
ea28fc783c | |||
c04752ed39 | |||
c198e6a2f7 | |||
2c1337bb51 | |||
7a4032dda4 | |||
1ab300d74f | |||
1b8c389984 | |||
74a20b2f65 | |||
d5c17285c1 | |||
e378b4c70a | |||
31854fc4b8 | |||
f52fe8306f | |||
b613b84469 | |||
2776d019b9 | |||
729e72cddb | |||
b978abfcbe | |||
ec101c1f52 | |||
cfec79405f | |||
7d8be1b9fc | |||
c781159e75 | |||
3cf6c295b0 | |||
e757585bd3 | |||
736d16a7ab | |||
6f4a8d5534 | |||
b5e17c4ff7 | |||
cc01525f16 | |||
c2ec05662b | |||
2d4a1bff83 | |||
cd853e4d57 | |||
8183d7d5a0 | |||
3f199cb610 | |||
22f72981cb | |||
bce92b7347 | |||
ed5166333a | |||
a05f1f70f7 | |||
e58a60410c | |||
fc91936884 | |||
88043569ac | |||
10add8a70e | |||
098af9884a | |||
554c1b1261 |
@ -162,7 +162,7 @@ jobs:
|
||||
openssl aes-256-cbc -d -in ./app/upload-key-encrypted.jks -k $ENCRYPT_KEY >> ./app/upload-key.jks
|
||||
- run:
|
||||
name: Publish release
|
||||
command: ./gradlew publishPlayRelease --no-daemon --stacktrace --console=plain -PenableCrashlytics -PdisablePreDex
|
||||
command: ./gradlew publishPlayRelease --no-daemon --stacktrace --console=plain -PdisablePreDex
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
|
4
.github/FUNDING.yml
vendored
Normal file
4
.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: wulkanowy
|
||||
custom: https://www.paypal.com/paypalme/wulkanowy
|
2
.github/workflows/deploy-store.yml
vendored
2
.github/workflows/deploy-store.yml
vendored
@ -40,7 +40,7 @@ jobs:
|
||||
SINGLE_SUPPORT_AD_ID: ${{ secrets.SINGLE_SUPPORT_AD_ID }}
|
||||
DASHBOARD_TILE_AD_ID: ${{ secrets.DASHBOARD_TILE_AD_ID }}
|
||||
SET_BUILD_TIMESTAMP: ${{ secrets.SET_BUILD_TIMESTAMP }}
|
||||
run: ./gradlew publishPlayReleaseApps -PenableFirebase --stacktrace;
|
||||
run: ./gradlew publishPlayReleaseApps --stacktrace;
|
||||
|
||||
deploy-app-gallery:
|
||||
name: AppGallery
|
||||
|
5
.github/workflows/deploy-test.yml
vendored
5
.github/workflows/deploy-test.yml
vendored
@ -36,8 +36,7 @@ jobs:
|
||||
- name: Prepare build configuration
|
||||
run: |
|
||||
sed -i -e "s#applicationIdSuffix \".dev\"#applicationIdSuffix \".${GITHUB_HEAD_REF//[-.\/]/_}\"#" app/build.gradle
|
||||
sed -i -e "s#.dev\"#.${GITHUB_HEAD_REF//[-.\/]/_}\"#" app/src/debug/google-services.json
|
||||
sed -i -e "s#.dev\"#.${GITHUB_HEAD_REF//[-.\/]/_}\"#" app/src/debug/agconnect-services.json
|
||||
sed -i -e "s#.dev\"#.${GITHUB_HEAD_REF//[-.\/]/_}\"#" app/google-services.json
|
||||
sed -i -e '/versionNameSuffix/d' app/build.gradle
|
||||
- name: Add signing config
|
||||
run: |
|
||||
@ -131,7 +130,7 @@ jobs:
|
||||
BITRISE_KEYSTORE_PASSWORD: ${{ secrets.BITRISE_KEYSTORE_PASSWORD }}
|
||||
BITRISE_KEY_ALIAS: ${{ secrets.BITRISE_KEY_ALIAS }}
|
||||
BITRISE_KEY_PASSWORD: ${{ secrets.BITRISE_KEY_PASSWORD }}
|
||||
run: ./gradlew assemblePlayDebug -PenableFirebase --stacktrace
|
||||
run: ./gradlew assemblePlayDebug --stacktrace
|
||||
- name: Upload apk to github artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
|
17
.gitignore
vendored
17
.gitignore
vendored
@ -65,6 +65,12 @@ captures/
|
||||
.idea/uiDesigner.xml
|
||||
.idea/runConfigurations.xml
|
||||
.idea/discord.xml
|
||||
.idea/migrations.xml
|
||||
.idea/androidTestResultsUserPreferences.xml
|
||||
.idea/copilot
|
||||
.idea/deploymentTargetDropDown.xml
|
||||
.idea/deploymentTargetSelector.xml
|
||||
.idea/kotlinc.xml
|
||||
|
||||
# Keystore files
|
||||
*.jks
|
||||
@ -111,12 +117,13 @@ Thumbs.db
|
||||
*.ear
|
||||
|
||||
### AndroidStudio Patch ###
|
||||
|
||||
!/gradle/wrapper/gradle-wrapper.jar
|
||||
.idea/jarRepositories.xml
|
||||
|
||||
### Services config files
|
||||
agconnect-services.json
|
||||
agconnect-credentials.json
|
||||
google-services.json
|
||||
!app/google-services.json
|
||||
|
||||
|
||||
app/src/release/agconnect-services.json
|
||||
app/src/release/agconnect-credentials.json
|
||||
.idea/deploymentTargetDropDown.xml
|
||||
.idea/kotlinc.xml
|
||||
|
10
.idea/migrations.xml
generated
10
.idea/migrations.xml
generated
@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectMigrations">
|
||||
<option name="MigrateToGradleLocalJavaHome">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
</set>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
@ -61,7 +61,7 @@ script:
|
||||
gpg --yes --batch --passphrase=$SERVICES_ENCRYPT_KEY ./app/src/release/agconnect-services.json.gpg;
|
||||
gpg --yes --batch --passphrase=$ENCRYPT_KEY ./app/key.p12.gpg;
|
||||
gpg --yes --batch --passphrase=$ENCRYPT_KEY ./app/upload-key.jks.gpg;
|
||||
./gradlew publishPlayRelease -PenableFirebase --stacktrace;
|
||||
./gradlew publishPlayRelease --stacktrace;
|
||||
fi
|
||||
|
||||
after_success:
|
||||
|
@ -27,15 +27,12 @@ android {
|
||||
testApplicationId "io.github.tests.wulkanowy"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 34
|
||||
versionCode 145
|
||||
versionName "2.3.5"
|
||||
versionCode 150
|
||||
versionName "2.5.1"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
resValue "string", "app_name", "Wulkanowy"
|
||||
manifestPlaceholders = [
|
||||
firebase_enabled: project.hasProperty("enableFirebase"),
|
||||
admob_project_id: ""
|
||||
]
|
||||
manifestPlaceholders = [admob_project_id: ""]
|
||||
|
||||
buildConfigField "String", "SINGLE_SUPPORT_AD_ID", "null"
|
||||
buildConfigField "String", "DASHBOARD_TILE_AD_ID", "null"
|
||||
@ -76,7 +73,6 @@ android {
|
||||
resValue "string", "app_name", "Wulkanowy DEV"
|
||||
applicationIdSuffix ".dev"
|
||||
versionNameSuffix "-dev"
|
||||
ext.enableCrashlytics = project.hasProperty("enableFirebase")
|
||||
buildConfigField "String", "MESSAGES_BASE_URL", "\"https://messages.wulkanowy.net.pl\""
|
||||
buildConfigField "String", "SCHOOLS_BASE_URL", '"https://schools.wulkanowy.net.pl"'
|
||||
}
|
||||
@ -142,7 +138,9 @@ android {
|
||||
packagingOptions {
|
||||
resources {
|
||||
excludes += ['META-INF/library_release.kotlin_module',
|
||||
'META-INF/library-core_release.kotlin_module']
|
||||
'META-INF/library-core_release.kotlin_module',
|
||||
'META-INF/LICENSE.md',
|
||||
'META-INF/LICENSE-notice.md']
|
||||
}
|
||||
}
|
||||
|
||||
@ -162,7 +160,7 @@ play {
|
||||
defaultToAppBundles = false
|
||||
track = 'production'
|
||||
releaseStatus = ReleaseStatus.IN_PROGRESS
|
||||
userFraction = 0.15d
|
||||
userFraction = 0.50d
|
||||
updatePriority = 1
|
||||
enabled.set(false)
|
||||
}
|
||||
@ -185,19 +183,19 @@ huaweiPublish {
|
||||
|
||||
ext {
|
||||
work_manager = "2.9.0"
|
||||
android_hilt = "1.1.0"
|
||||
android_hilt = "1.2.0"
|
||||
room = "2.6.1"
|
||||
chucker = "4.0.0"
|
||||
mockk = "1.13.9"
|
||||
coroutines = "1.7.3"
|
||||
mockk = "1.13.10"
|
||||
coroutines = "1.8.0"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'io.github.wulkanowy:sdk:2.3.7'
|
||||
implementation 'io.github.wulkanowy:sdk:2.5.1'
|
||||
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
|
||||
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines"
|
||||
|
||||
implementation 'androidx.core:core-ktx:1.12.0'
|
||||
@ -244,25 +242,25 @@ dependencies {
|
||||
implementation 'com.github.Faierbel:slf4j-timber:2.0'
|
||||
implementation 'com.github.bastienpaulfr:Treessence:1.1.2'
|
||||
implementation "com.mikepenz:aboutlibraries-core:$about_libraries"
|
||||
implementation 'io.coil-kt:coil:2.5.0'
|
||||
implementation 'io.coil-kt:coil:2.6.0'
|
||||
implementation "io.github.wulkanowy:AppKillerManager:3.0.1"
|
||||
implementation 'me.xdrop:fuzzywuzzy:1.4.0'
|
||||
implementation 'com.fredporciuncula:flow-preferences:1.9.1'
|
||||
implementation 'org.apache.commons:commons-text:1.11.0'
|
||||
|
||||
playImplementation platform('com.google.firebase:firebase-bom:32.7.0')
|
||||
playImplementation platform('com.google.firebase:firebase-bom:32.7.4')
|
||||
playImplementation 'com.google.firebase:firebase-analytics'
|
||||
playImplementation 'com.google.firebase:firebase-messaging'
|
||||
playImplementation 'com.google.firebase:firebase-crashlytics:'
|
||||
playImplementation 'com.google.firebase:firebase-config'
|
||||
|
||||
playImplementation 'com.google.android.gms:play-services-ads:22.6.0'
|
||||
playImplementation 'com.google.android.gms:play-services-ads:23.0.0'
|
||||
playImplementation "com.google.android.play:integrity:1.3.0"
|
||||
playImplementation 'com.google.android.play:app-update-ktx:2.1.0'
|
||||
playImplementation 'com.google.android.play:review-ktx:2.0.1'
|
||||
playImplementation "com.google.android.ump:user-messaging-platform:2.1.0"
|
||||
|
||||
hmsImplementation 'com.huawei.hms:hianalytics:6.12.0.300'
|
||||
hmsImplementation 'com.huawei.hms:hianalytics:6.12.0.301'
|
||||
hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.9.1.303'
|
||||
|
||||
releaseImplementation "com.github.chuckerteam.chucker:library-no-op:$chucker"
|
||||
|
@ -1,7 +1,8 @@
|
||||
#!/bin/bash -
|
||||
|
||||
content=$(cat < "app/src/main/play/release-notes/pl-PL/default.txt") || exit
|
||||
if [[ "${#content}" -gt 500 ]]; then
|
||||
content2=echo "$content" | dos2unix
|
||||
if [[ "${#content2}" -gt 500 ]]; then
|
||||
echo >&2 "Release notes content has reached the limit of 500 characters"
|
||||
exit 1
|
||||
fi
|
||||
|
2501
app/schemas/io.github.wulkanowy.data.db.AppDatabase/59.json
Normal file
2501
app/schemas/io.github.wulkanowy.data.db.AppDatabase/59.json
Normal file
File diff suppressed because it is too large
Load Diff
2527
app/schemas/io.github.wulkanowy.data.db.AppDatabase/60.json
Normal file
2527
app/schemas/io.github.wulkanowy.data.db.AppDatabase/60.json
Normal file
File diff suppressed because it is too large
Load Diff
2533
app/schemas/io.github.wulkanowy.data.db.AppDatabase/61.json
Normal file
2533
app/schemas/io.github.wulkanowy.data.db.AppDatabase/61.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -14,34 +14,37 @@ import kotlin.test.assertFailsWith
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ScramblerTest {
|
||||
|
||||
private val scrambler = Scrambler(ApplicationProvider.getApplicationContext())
|
||||
|
||||
@Test
|
||||
fun encryptDecryptTest() {
|
||||
assertEquals("TEST", decrypt(encrypt("TEST",
|
||||
ApplicationProvider.getApplicationContext())))
|
||||
assertEquals(
|
||||
"TEST", scrambler.decrypt(scrambler.encrypt("TEST"))
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun emptyTextEncryptTest() {
|
||||
assertFailsWith<ScramblerException> {
|
||||
decrypt("")
|
||||
scrambler.decrypt("")
|
||||
}
|
||||
|
||||
assertFailsWith<ScramblerException> {
|
||||
encrypt("", ApplicationProvider.getApplicationContext())
|
||||
scrambler.encrypt("")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@SdkSuppress(minSdkVersion = 18)
|
||||
fun emptyKeyStoreTest() {
|
||||
val text = encrypt("test", ApplicationProvider.getApplicationContext())
|
||||
val text = scrambler.encrypt("test")
|
||||
|
||||
val keyStore = KeyStore.getInstance("AndroidKeyStore")
|
||||
keyStore.load(null)
|
||||
keyStore.deleteEntry("wulkanowy_password")
|
||||
|
||||
assertFailsWith<ScramblerException> {
|
||||
decrypt(text)
|
||||
scrambler.decrypt(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@ -44,6 +44,7 @@
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:supportsRtl="false"
|
||||
android:theme="@style/WulkanowyTheme"
|
||||
android:resizeableActivity="true"
|
||||
tools:ignore="DataExtractionRules,UnusedAttribute">
|
||||
<activity
|
||||
android:name=".ui.modules.splash.SplashActivity"
|
||||
@ -154,33 +155,9 @@
|
||||
android:resource="@xml/provider_paths" />
|
||||
</provider>
|
||||
|
||||
<!-- workaround for https://github.com/firebase/firebase-android-sdk/issues/473 enabled:false -->
|
||||
<!-- https://firebase.googleblog.com/2017/03/take-control-of-your-firebase-init-on.html -->
|
||||
<provider
|
||||
android:name="com.google.firebase.provider.FirebaseInitProvider"
|
||||
android:authorities="${applicationId}.firebaseinitprovider"
|
||||
android:enabled="${firebase_enabled}"
|
||||
android:exported="false"
|
||||
tools:ignore="MissingClass" />
|
||||
|
||||
<meta-data
|
||||
android:name="install_channel"
|
||||
android:value="${install_channel}" />
|
||||
<meta-data
|
||||
android:name="firebase_analytics_collection_enabled"
|
||||
android:value="${firebase_enabled}" />
|
||||
<meta-data
|
||||
android:name="google_analytics_adid_collection_enabled"
|
||||
android:value="${firebase_enabled}" />
|
||||
<meta-data
|
||||
android:name="firebase_crashlytics_collection_enabled"
|
||||
android:value="${firebase_enabled}" />
|
||||
<meta-data
|
||||
android:name="firebase_messaging_auto_init_enabled"
|
||||
android:value="${firebase_enabled}" />
|
||||
<meta-data
|
||||
android:name="firebase_inapp_messaging_auto_data_collection_enabled"
|
||||
android:value="${firebase_enabled}" />
|
||||
<meta-data
|
||||
android:name="com.google.firebase.messaging.default_notification_icon"
|
||||
android:resource="@drawable/ic_stat_all" />
|
||||
|
@ -54,5 +54,9 @@
|
||||
{
|
||||
"displayName": "Antoni Paduch",
|
||||
"githubUsername": "janAte1"
|
||||
},
|
||||
{
|
||||
"displayName": "Kamil Wąsik",
|
||||
"githubUsername": "JestemKamil"
|
||||
}
|
||||
]
|
||||
|
@ -18,17 +18,13 @@ import io.github.wulkanowy.data.api.SchoolsService
|
||||
import io.github.wulkanowy.data.db.AppDatabase
|
||||
import io.github.wulkanowy.data.db.SharedPrefProvider
|
||||
import io.github.wulkanowy.data.repositories.PreferencesRepository
|
||||
import io.github.wulkanowy.sdk.Sdk
|
||||
import io.github.wulkanowy.utils.AppInfo
|
||||
import io.github.wulkanowy.utils.RemoteConfigHelper
|
||||
import io.github.wulkanowy.utils.WebkitCookieManagerProxy
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.create
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Singleton
|
||||
|
||||
@ -36,20 +32,6 @@ import javax.inject.Singleton
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal class DataModule {
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideSdk(chuckerInterceptor: ChuckerInterceptor, remoteConfig: RemoteConfigHelper) =
|
||||
Sdk().apply {
|
||||
androidVersion = android.os.Build.VERSION.RELEASE
|
||||
buildTag = android.os.Build.MODEL
|
||||
userAgentTemplate = remoteConfig.userAgentTemplate
|
||||
setSimpleHttpLogger { Timber.d(it) }
|
||||
setAdditionalCookieManager(WebkitCookieManagerProxy())
|
||||
|
||||
// for debug only
|
||||
addInterceptor(chuckerInterceptor, network = true)
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideChuckerCollector(
|
||||
@ -253,4 +235,12 @@ internal class DataModule {
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideAdminMessageDao(database: AppDatabase) = database.adminMessagesDao
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideMutesDao(database: AppDatabase) = database.mutedMessageSendersDao
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideGradeDescriptiveDao(database: AppDatabase) = database.gradeDescriptiveDao
|
||||
}
|
||||
|
@ -1,11 +1,17 @@
|
||||
package io.github.wulkanowy.data
|
||||
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.filterNot
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
@ -14,24 +20,54 @@ import kotlinx.coroutines.flow.takeWhile
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import timber.log.Timber
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
sealed class Resource<T> {
|
||||
|
||||
open class Loading<T> : Resource<T>()
|
||||
sealed interface Resource<out T> {
|
||||
/**
|
||||
* The initial value of a resource flow. Indicates no data that is currently available to be shown,
|
||||
* however with the expectation that the state will transition to another one soon.
|
||||
*/
|
||||
open class Loading<T> : Resource<T>
|
||||
|
||||
/**
|
||||
* A semi-loading state with some data available to be displayed (usually cached data loaded from
|
||||
* the database). Still not the target state and it's expected to transition into another one soon.
|
||||
*/
|
||||
data class Intermediate<T>(val data: T) : Loading<T>()
|
||||
|
||||
data class Success<T>(val data: T) : Resource<T>()
|
||||
/**
|
||||
* The happy-path target state. Data can either be:
|
||||
* - loaded from the database - while it may seem like this case is already handled by the
|
||||
* Intermediate state, the difference here is semantic. Cached data is returned as Intermediate
|
||||
* when there's a API request in progress (or soon expected to be), however when there is no
|
||||
* intention of immediately querying the API, the cached data is returned as a Success.
|
||||
* - fetched from the API.
|
||||
*/
|
||||
data class Success<T>(val data: T) : Resource<T>
|
||||
|
||||
data class Error<T>(val error: Throwable) : Resource<T>()
|
||||
/**
|
||||
* Something bad happened and we were unable to get the requested data. This can be caused by
|
||||
* a database error, a network error, or really just any other error. Upon receiving this state
|
||||
* the UI can either: display a full screen error, or, when it has received any data previously,
|
||||
* display a snack bar informing of the problem.
|
||||
*/
|
||||
data class Error<T>(val error: Throwable) : Resource<T>
|
||||
}
|
||||
|
||||
val <T> Resource<T>.dataOrNull: T?
|
||||
get() = when (this) {
|
||||
is Resource.Success -> this.data
|
||||
is Resource.Intermediate -> this.data
|
||||
is Resource.Loading -> null
|
||||
is Resource.Error -> null
|
||||
else -> null
|
||||
}
|
||||
|
||||
val <T> Resource<T>.dataOrThrow: T
|
||||
get() = when (this) {
|
||||
is Resource.Success -> this.data
|
||||
is Resource.Intermediate -> this.data
|
||||
is Resource.Loading -> throw IllegalStateException("Resource is in loading state")
|
||||
is Resource.Error -> throw this.error
|
||||
}
|
||||
|
||||
val <T> Resource<T>.errorOrNull: Throwable?
|
||||
@ -57,6 +93,22 @@ fun <T, U> Resource<T>.mapData(block: (T) -> U) = when (this) {
|
||||
is Resource.Error -> Resource.Error(this.error)
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects another flow into this flow's resource data.
|
||||
*/
|
||||
inline fun <T1, T2, R> Flow<Resource<T1>>.combineWithResourceData(
|
||||
flow: Flow<T2>,
|
||||
crossinline block: suspend (T1, T2) -> R
|
||||
): Flow<Resource<R>> =
|
||||
combine(flow) { resource, inject ->
|
||||
when (resource) {
|
||||
is Resource.Success -> Resource.Success(block(resource.data, inject))
|
||||
is Resource.Intermediate -> Resource.Intermediate(block(resource.data, inject))
|
||||
is Resource.Loading -> Resource.Loading()
|
||||
is Resource.Error -> Resource.Error(resource.error)
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> Flow<Resource<T>>.logResourceStatus(name: String, showData: Boolean = false) = onEach {
|
||||
val description = when (it) {
|
||||
is Resource.Intermediate -> "intermediate data received" + if (showData) " (data: `${it.data}`)" else ""
|
||||
@ -67,8 +119,29 @@ fun <T> Flow<Resource<T>>.logResourceStatus(name: String, showData: Boolean = fa
|
||||
Timber.i("$name: $description")
|
||||
}
|
||||
|
||||
fun <T, U> Flow<Resource<T>>.mapResourceData(block: (T) -> U) = map {
|
||||
it.mapData(block)
|
||||
inline fun <T, U> Flow<Resource<T>>.mapResourceData(crossinline block: suspend (T) -> U) = map {
|
||||
when (it) {
|
||||
is Resource.Success -> Resource.Success(block(it.data))
|
||||
is Resource.Intermediate -> Resource.Intermediate(block(it.data))
|
||||
is Resource.Loading -> Resource.Loading()
|
||||
is Resource.Error -> Resource.Error(it.error)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun <T, U> Flow<Resource<T>>.flatMapResourceData(
|
||||
inheritIntermediate: Boolean = true, block: suspend (T) -> Flow<Resource<U>>
|
||||
) = flatMapLatest {
|
||||
when (it) {
|
||||
is Resource.Success -> block(it.data)
|
||||
is Resource.Intermediate -> block(it.data).map { newRes ->
|
||||
if (inheritIntermediate && newRes is Resource.Success) Resource.Intermediate(newRes.data)
|
||||
else newRes
|
||||
}
|
||||
|
||||
is Resource.Loading -> flowOf(Resource.Loading())
|
||||
is Resource.Error -> flowOf(Resource.Error(it.error))
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> Flow<Resource<T>>.onResourceData(block: suspend (T) -> Unit) = onEach {
|
||||
@ -98,13 +171,13 @@ fun <T> Flow<Resource<T>>.onResourceSuccess(block: suspend (T) -> Unit) = onEach
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> Flow<Resource<T>>.onResourceError(block: (Throwable) -> Unit) = onEach {
|
||||
fun <T> Flow<Resource<T>>.onResourceError(block: suspend (Throwable) -> Unit) = onEach {
|
||||
if (it is Resource.Error) {
|
||||
block(it.error)
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> Flow<Resource<T>>.onResourceNotLoading(block: () -> Unit) = onEach {
|
||||
fun <T> Flow<Resource<T>>.onResourceNotLoading(block: suspend () -> Unit) = onEach {
|
||||
if (it !is Resource.Loading) {
|
||||
block()
|
||||
}
|
||||
@ -114,70 +187,99 @@ suspend fun <T> Flow<Resource<T>>.toFirstResult() = filter { it !is Resource.Loa
|
||||
|
||||
suspend fun <T> Flow<Resource<T>>.waitForResult() = takeWhile { it is Resource.Loading }.collect()
|
||||
|
||||
inline fun <ResultType, RequestType> networkBoundResource(
|
||||
mutex: Mutex = Mutex(),
|
||||
showSavedOnLoading: Boolean = true,
|
||||
crossinline isResultEmpty: (ResultType) -> Boolean,
|
||||
crossinline query: () -> Flow<ResultType>,
|
||||
crossinline fetch: suspend (ResultType) -> RequestType,
|
||||
crossinline saveFetchResult: suspend (old: ResultType, new: RequestType) -> Unit,
|
||||
crossinline onFetchFailed: (Throwable) -> Unit = { },
|
||||
crossinline shouldFetch: (ResultType) -> Boolean = { true },
|
||||
crossinline filterResult: (ResultType) -> ResultType = { it }
|
||||
) = flow {
|
||||
emit(Resource.Loading())
|
||||
// Can cause excessive amounts of `Resource.Intermediate` to be emitted. Unless that is desired,
|
||||
// use `debounceIntermediates` to alleviate this behavior.
|
||||
inline fun <reified T> combineResourceFlows(flows: Iterable<Flow<Resource<T>>>): Flow<Resource<List<T>>> =
|
||||
combine(flows) { items ->
|
||||
var isIntermediate = false
|
||||
val data = mutableListOf<T>()
|
||||
for (item in items) {
|
||||
when (item) {
|
||||
is Resource.Success -> data.add(item.data)
|
||||
is Resource.Intermediate -> {
|
||||
isIntermediate = true
|
||||
data.add(item.data)
|
||||
}
|
||||
|
||||
val data = query().first()
|
||||
emitAll(if (shouldFetch(data)) {
|
||||
val filteredResult = filterResult(data)
|
||||
|
||||
if (showSavedOnLoading && !isResultEmpty(filteredResult)) {
|
||||
emit(Resource.Intermediate(filteredResult))
|
||||
is Resource.Loading -> return@combine Resource.Loading()
|
||||
is Resource.Error -> continue
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
val newData = fetch(data)
|
||||
mutex.withLock { saveFetchResult(query().first(), newData) }
|
||||
query().map { Resource.Success(filterResult(it)) }
|
||||
} catch (throwable: Throwable) {
|
||||
onFetchFailed(throwable)
|
||||
flowOf(Resource.Error(throwable))
|
||||
if (data.isEmpty()) {
|
||||
// All items have to be errors for this to happen, so just return the first one.
|
||||
// mapData is functionally useless and exists only to satisfy the type checker
|
||||
items.first().mapData { listOf(it) }
|
||||
} else if (isIntermediate) {
|
||||
Resource.Intermediate(data)
|
||||
} else {
|
||||
Resource.Success(data)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
fun <T> Flow<Resource<T>>.debounceIntermediates(timeout: Duration = 5.seconds) = flow {
|
||||
var wasIntermediate = false
|
||||
|
||||
emitAll(this@debounceIntermediates.debounce {
|
||||
if (it is Resource.Intermediate) {
|
||||
if (!wasIntermediate) {
|
||||
wasIntermediate = true
|
||||
Duration.ZERO
|
||||
} else {
|
||||
timeout
|
||||
}
|
||||
} else {
|
||||
wasIntermediate = false
|
||||
Duration.ZERO
|
||||
}
|
||||
} else {
|
||||
query().map { Resource.Success(filterResult(it)) }
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
inline fun <OutputType, ApiType> networkBoundResource(
|
||||
mutex: Mutex = Mutex(),
|
||||
crossinline isResultEmpty: (OutputType) -> Boolean,
|
||||
crossinline query: () -> Flow<OutputType>,
|
||||
crossinline fetch: suspend () -> ApiType,
|
||||
crossinline saveFetchResult: suspend (old: OutputType, new: ApiType) -> Unit,
|
||||
crossinline shouldFetch: (OutputType) -> Boolean = { true },
|
||||
crossinline filterResult: (OutputType) -> OutputType = { it }
|
||||
) = networkBoundResource(
|
||||
mutex = mutex,
|
||||
isResultEmpty = isResultEmpty,
|
||||
query = query,
|
||||
fetch = fetch,
|
||||
saveFetchResult = saveFetchResult,
|
||||
shouldFetch = shouldFetch,
|
||||
mapResult = filterResult
|
||||
)
|
||||
|
||||
@JvmName("networkBoundResourceWithMap")
|
||||
inline fun <ResultType, RequestType, T> networkBoundResource(
|
||||
inline fun <DatabaseType, ApiType, OutputType> networkBoundResource(
|
||||
mutex: Mutex = Mutex(),
|
||||
showSavedOnLoading: Boolean = true,
|
||||
crossinline isResultEmpty: (T) -> Boolean,
|
||||
crossinline query: () -> Flow<ResultType>,
|
||||
crossinline fetch: suspend (ResultType) -> RequestType,
|
||||
crossinline saveFetchResult: suspend (old: ResultType, new: RequestType) -> Unit,
|
||||
crossinline onFetchFailed: (Throwable) -> Unit = { },
|
||||
crossinline shouldFetch: (ResultType) -> Boolean = { true },
|
||||
crossinline mapResult: (ResultType) -> T,
|
||||
crossinline isResultEmpty: (OutputType) -> Boolean,
|
||||
crossinline query: () -> Flow<DatabaseType>,
|
||||
crossinline fetch: suspend () -> ApiType,
|
||||
crossinline saveFetchResult: suspend (old: DatabaseType, new: ApiType) -> Unit,
|
||||
crossinline shouldFetch: (DatabaseType) -> Boolean = { true },
|
||||
crossinline mapResult: (DatabaseType) -> OutputType,
|
||||
) = flow {
|
||||
emit(Resource.Loading())
|
||||
|
||||
val data = query().first()
|
||||
emitAll(if (shouldFetch(data)) {
|
||||
val mappedResult = mapResult(data)
|
||||
if (shouldFetch(data)) {
|
||||
emit(Resource.Intermediate(data))
|
||||
|
||||
if (showSavedOnLoading && !isResultEmpty(mappedResult)) {
|
||||
emit(Resource.Intermediate(mappedResult))
|
||||
}
|
||||
try {
|
||||
val newData = fetch(data)
|
||||
val newData = fetch()
|
||||
mutex.withLock { saveFetchResult(query().first(), newData) }
|
||||
query().map { Resource.Success(mapResult(it)) }
|
||||
} catch (throwable: Throwable) {
|
||||
onFetchFailed(throwable)
|
||||
flowOf(Resource.Error(throwable))
|
||||
emit(Resource.Error(throwable))
|
||||
return@flow
|
||||
}
|
||||
} else {
|
||||
query().map { Resource.Success(mapResult(it)) }
|
||||
})
|
||||
}
|
||||
|
||||
emitAll(query().map { Resource.Success(it) })
|
||||
}
|
||||
.mapResourceData { mapResult(it) }
|
||||
.filterNot { it is Resource.Intermediate && isResultEmpty(it.data) }
|
||||
|
@ -0,0 +1,64 @@
|
||||
package io.github.wulkanowy.data
|
||||
|
||||
import com.chuckerteam.chucker.api.ChuckerInterceptor
|
||||
import io.github.wulkanowy.data.db.entities.Semester
|
||||
import io.github.wulkanowy.data.db.entities.Student
|
||||
import io.github.wulkanowy.sdk.Sdk
|
||||
import io.github.wulkanowy.utils.RemoteConfigHelper
|
||||
import io.github.wulkanowy.utils.WebkitCookieManagerProxy
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class WulkanowySdkFactory @Inject constructor(
|
||||
private val chuckerInterceptor: ChuckerInterceptor,
|
||||
private val remoteConfig: RemoteConfigHelper,
|
||||
private val webkitCookieManagerProxy: WebkitCookieManagerProxy
|
||||
) {
|
||||
|
||||
private val sdk = Sdk().apply {
|
||||
androidVersion = android.os.Build.VERSION.RELEASE
|
||||
buildTag = android.os.Build.MODEL
|
||||
userAgentTemplate = remoteConfig.userAgentTemplate
|
||||
setSimpleHttpLogger { Timber.d(it) }
|
||||
setAdditionalCookieManager(webkitCookieManagerProxy)
|
||||
|
||||
// for debug only
|
||||
addInterceptor(chuckerInterceptor, network = true)
|
||||
}
|
||||
|
||||
fun create() = sdk
|
||||
|
||||
fun create(student: Student, semester: Semester? = null): Sdk {
|
||||
return create().apply {
|
||||
email = student.email
|
||||
password = student.password
|
||||
symbol = student.symbol
|
||||
schoolSymbol = student.schoolSymbol
|
||||
studentId = student.studentId
|
||||
classId = student.classId
|
||||
emptyCookieJarInterceptor = true
|
||||
|
||||
if (Sdk.Mode.valueOf(student.loginMode) == Sdk.Mode.HEBE) {
|
||||
mobileBaseUrl = student.mobileBaseUrl
|
||||
} else {
|
||||
scrapperBaseUrl = student.scrapperBaseUrl
|
||||
domainSuffix = student.scrapperDomainSuffix
|
||||
loginType = Sdk.ScrapperLoginType.valueOf(student.loginType)
|
||||
}
|
||||
|
||||
mode = Sdk.Mode.valueOf(student.loginMode)
|
||||
mobileBaseUrl = student.mobileBaseUrl
|
||||
keyId = student.certificateKey
|
||||
privatePem = student.privateKey
|
||||
|
||||
if (semester != null) {
|
||||
diaryId = semester.diaryId
|
||||
kindergartenDiaryId = semester.kindergartenDiaryId
|
||||
schoolYear = semester.schoolYear
|
||||
unitId = semester.unitId
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -14,6 +14,7 @@ import io.github.wulkanowy.data.db.dao.CompletedLessonsDao
|
||||
import io.github.wulkanowy.data.db.dao.ConferenceDao
|
||||
import io.github.wulkanowy.data.db.dao.ExamDao
|
||||
import io.github.wulkanowy.data.db.dao.GradeDao
|
||||
import io.github.wulkanowy.data.db.dao.GradeDescriptiveDao
|
||||
import io.github.wulkanowy.data.db.dao.GradePartialStatisticsDao
|
||||
import io.github.wulkanowy.data.db.dao.GradePointsStatisticsDao
|
||||
import io.github.wulkanowy.data.db.dao.GradeSemesterStatisticsDao
|
||||
@ -24,6 +25,7 @@ import io.github.wulkanowy.data.db.dao.MailboxDao
|
||||
import io.github.wulkanowy.data.db.dao.MessageAttachmentDao
|
||||
import io.github.wulkanowy.data.db.dao.MessagesDao
|
||||
import io.github.wulkanowy.data.db.dao.MobileDeviceDao
|
||||
import io.github.wulkanowy.data.db.dao.MutedMessageSendersDao
|
||||
import io.github.wulkanowy.data.db.dao.NoteDao
|
||||
import io.github.wulkanowy.data.db.dao.NotificationDao
|
||||
import io.github.wulkanowy.data.db.dao.RecipientDao
|
||||
@ -44,6 +46,7 @@ import io.github.wulkanowy.data.db.entities.CompletedLesson
|
||||
import io.github.wulkanowy.data.db.entities.Conference
|
||||
import io.github.wulkanowy.data.db.entities.Exam
|
||||
import io.github.wulkanowy.data.db.entities.Grade
|
||||
import io.github.wulkanowy.data.db.entities.GradeDescriptive
|
||||
import io.github.wulkanowy.data.db.entities.GradePartialStatistics
|
||||
import io.github.wulkanowy.data.db.entities.GradePointsStatistics
|
||||
import io.github.wulkanowy.data.db.entities.GradeSemesterStatistics
|
||||
@ -54,6 +57,7 @@ import io.github.wulkanowy.data.db.entities.Mailbox
|
||||
import io.github.wulkanowy.data.db.entities.Message
|
||||
import io.github.wulkanowy.data.db.entities.MessageAttachment
|
||||
import io.github.wulkanowy.data.db.entities.MobileDevice
|
||||
import io.github.wulkanowy.data.db.entities.MutedMessageSender
|
||||
import io.github.wulkanowy.data.db.entities.Note
|
||||
import io.github.wulkanowy.data.db.entities.Notification
|
||||
import io.github.wulkanowy.data.db.entities.Recipient
|
||||
@ -154,7 +158,9 @@ import javax.inject.Singleton
|
||||
TimetableHeader::class,
|
||||
SchoolAnnouncement::class,
|
||||
Notification::class,
|
||||
AdminMessage::class
|
||||
AdminMessage::class,
|
||||
MutedMessageSender::class,
|
||||
GradeDescriptive::class,
|
||||
],
|
||||
autoMigrations = [
|
||||
AutoMigration(from = 44, to = 45),
|
||||
@ -165,6 +171,9 @@ import javax.inject.Singleton
|
||||
AutoMigration(from = 55, to = 56),
|
||||
AutoMigration(from = 56, to = 57, spec = Migration57::class),
|
||||
AutoMigration(from = 57, to = 58, spec = Migration58::class),
|
||||
AutoMigration(from = 58, to = 59),
|
||||
AutoMigration(from = 59, to = 60),
|
||||
AutoMigration(from = 60, to = 61),
|
||||
],
|
||||
version = AppDatabase.VERSION_SCHEMA,
|
||||
exportSchema = true
|
||||
@ -173,7 +182,7 @@ import javax.inject.Singleton
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
|
||||
companion object {
|
||||
const val VERSION_SCHEMA = 58
|
||||
const val VERSION_SCHEMA = 61
|
||||
|
||||
fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf(
|
||||
Migration2(),
|
||||
@ -298,4 +307,8 @@ abstract class AppDatabase : RoomDatabase() {
|
||||
abstract val notificationDao: NotificationDao
|
||||
|
||||
abstract val adminMessagesDao: AdminMessageDao
|
||||
|
||||
abstract val mutedMessageSendersDao: MutedMessageSendersDao
|
||||
|
||||
abstract val gradeDescriptiveDao: GradeDescriptiveDao
|
||||
}
|
||||
|
@ -2,24 +2,14 @@ package io.github.wulkanowy.data.db.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import io.github.wulkanowy.data.db.entities.AdminMessage
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
@Dao
|
||||
abstract class AdminMessageDao : BaseDao<AdminMessage> {
|
||||
interface AdminMessageDao : BaseDao<AdminMessage> {
|
||||
|
||||
@Query("SELECT * FROM AdminMessages")
|
||||
abstract fun loadAll(): Flow<List<AdminMessage>>
|
||||
|
||||
@Transaction
|
||||
open suspend fun removeOldAndSaveNew(
|
||||
oldMessages: List<AdminMessage>,
|
||||
newMessages: List<AdminMessage>
|
||||
) {
|
||||
deleteAll(oldMessages)
|
||||
insertAll(newMessages)
|
||||
}
|
||||
fun loadAll(): Flow<List<AdminMessage>>
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ package io.github.wulkanowy.data.db.dao
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Transaction
|
||||
import androidx.room.Update
|
||||
|
||||
interface BaseDao<T> {
|
||||
@ -15,4 +16,10 @@ interface BaseDao<T> {
|
||||
|
||||
@Delete
|
||||
suspend fun deleteAll(items: List<T>)
|
||||
|
||||
@Transaction
|
||||
suspend fun removeOldAndSaveNew(oldItems: List<T>, newItems: List<T>) {
|
||||
deleteAll(oldItems)
|
||||
insertAll(newItems)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,15 @@
|
||||
package io.github.wulkanowy.data.db.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import io.github.wulkanowy.data.db.entities.GradeDescriptive
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
@Dao
|
||||
interface GradeDescriptiveDao : BaseDao<GradeDescriptive> {
|
||||
|
||||
@Query("SELECT * FROM GradesDescriptive WHERE semester_id = :semesterId AND student_id = :studentId")
|
||||
fun loadAll(semesterId: Int, studentId: Int): Flow<List<GradeDescriptive>>
|
||||
}
|
@ -5,15 +5,23 @@ import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import io.github.wulkanowy.data.db.entities.Message
|
||||
import io.github.wulkanowy.data.db.entities.MessageWithAttachment
|
||||
import io.github.wulkanowy.data.db.entities.MessageWithMutedAuthor
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface MessagesDao : BaseDao<Message> {
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM Messages WHERE message_global_key = :messageGlobalKey")
|
||||
fun loadMessageWithAttachment(messageGlobalKey: String): Flow<MessageWithAttachment?>
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM Messages WHERE mailbox_key = :mailboxKey AND folder_id = :folder ORDER BY date DESC")
|
||||
fun loadMessagesWithMutedAuthor(mailboxKey: String, folder: Int): Flow<List<MessageWithMutedAuthor>>
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM Messages WHERE email = :email AND folder_id = :folder ORDER BY date DESC")
|
||||
fun loadMessagesWithMutedAuthor(folder: Int, email: String): Flow<List<MessageWithMutedAuthor>>
|
||||
|
||||
@Query("SELECT * FROM Messages WHERE mailbox_key = :mailboxKey AND folder_id = :folder ORDER BY date DESC")
|
||||
fun loadAll(mailboxKey: String, folder: Int): Flow<List<Message>>
|
||||
|
||||
|
@ -0,0 +1,20 @@
|
||||
package io.github.wulkanowy.data.db.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import io.github.wulkanowy.data.db.entities.MutedMessageSender
|
||||
|
||||
@Dao
|
||||
interface MutedMessageSendersDao : BaseDao<MutedMessageSender> {
|
||||
|
||||
@Query("SELECT COUNT(*) FROM MutedMessageSenders WHERE author = :author")
|
||||
suspend fun checkMute(author: String): Boolean
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
suspend fun insertMute(mute: MutedMessageSender): Long
|
||||
|
||||
@Query("DELETE FROM MutedMessageSenders WHERE author = :author")
|
||||
suspend fun deleteMute(author: String)
|
||||
}
|
@ -15,5 +15,5 @@ interface TimetableDao : BaseDao<Timetable> {
|
||||
fun loadAll(diaryId: Int, studentId: Int, from: LocalDate, end: LocalDate): Flow<List<Timetable>>
|
||||
|
||||
@Query("SELECT * FROM Timetable WHERE diary_id = :diaryId AND student_id = :studentId AND date >= :from AND date <= :end")
|
||||
fun load(diaryId: Int, studentId: Int, from: LocalDate, end: LocalDate): List<Timetable>
|
||||
suspend fun load(diaryId: Int, studentId: Int, from: LocalDate, end: LocalDate): List<Timetable>
|
||||
}
|
||||
|
@ -0,0 +1,27 @@
|
||||
package io.github.wulkanowy.data.db.entities
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import java.io.Serializable
|
||||
|
||||
@Entity(tableName = "GradesDescriptive")
|
||||
data class GradeDescriptive(
|
||||
|
||||
@ColumnInfo(name = "semester_id")
|
||||
val semesterId: Int,
|
||||
|
||||
@ColumnInfo(name = "student_id")
|
||||
val studentId: Int,
|
||||
|
||||
val subject: String,
|
||||
|
||||
val description: String,
|
||||
) : Serializable {
|
||||
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
var id: Long = 0
|
||||
|
||||
@ColumnInfo(name = "is_notified")
|
||||
var isNotified: Boolean = true
|
||||
}
|
@ -2,11 +2,15 @@ package io.github.wulkanowy.data.db.entities
|
||||
|
||||
import androidx.room.Embedded
|
||||
import androidx.room.Relation
|
||||
import java.io.Serializable
|
||||
|
||||
data class MessageWithAttachment(
|
||||
@Embedded
|
||||
val message: Message,
|
||||
|
||||
@Relation(parentColumn = "message_global_key", entityColumn = "message_global_key")
|
||||
val attachments: List<MessageAttachment>
|
||||
)
|
||||
val attachments: List<MessageAttachment>,
|
||||
|
||||
@Relation(parentColumn = "correspondents", entityColumn = "author")
|
||||
val mutedMessageSender: MutedMessageSender?,
|
||||
) : Serializable
|
||||
|
@ -0,0 +1,12 @@
|
||||
package io.github.wulkanowy.data.db.entities
|
||||
|
||||
import androidx.room.Embedded
|
||||
import androidx.room.Relation
|
||||
|
||||
data class MessageWithMutedAuthor(
|
||||
@Embedded
|
||||
val message: Message,
|
||||
|
||||
@Relation(parentColumn = "correspondents", entityColumn = "author")
|
||||
val mutedMessageSender: MutedMessageSender?,
|
||||
)
|
@ -0,0 +1,15 @@
|
||||
package io.github.wulkanowy.data.db.entities
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import java.io.Serializable
|
||||
|
||||
@Entity(tableName = "MutedMessageSenders")
|
||||
data class MutedMessageSender(
|
||||
@ColumnInfo(name = "author")
|
||||
val author: String,
|
||||
) : Serializable {
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
var id: Long = 0
|
||||
}
|
@ -16,7 +16,9 @@ data class SchoolAnnouncement(
|
||||
|
||||
val subject: String,
|
||||
|
||||
val content: String
|
||||
val content: String,
|
||||
|
||||
val author: String? = null,
|
||||
) : Serializable {
|
||||
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -3,5 +3,10 @@ package io.github.wulkanowy.data.enums
|
||||
enum class MessageFolder(val id: Int = 1) {
|
||||
RECEIVED(1),
|
||||
SENT(2),
|
||||
TRASHED(3)
|
||||
TRASHED(3),
|
||||
;
|
||||
|
||||
companion object {
|
||||
fun byId(id: Int) = entries.first { it.id == id }
|
||||
}
|
||||
}
|
||||
|
@ -3,12 +3,26 @@ package io.github.wulkanowy.data.mappers
|
||||
import io.github.wulkanowy.data.db.entities.SchoolAnnouncement
|
||||
import io.github.wulkanowy.data.db.entities.Student
|
||||
import io.github.wulkanowy.sdk.pojo.DirectorInformation as SdkDirectorInformation
|
||||
import io.github.wulkanowy.sdk.pojo.LastAnnouncement as SdkLastAnnouncement
|
||||
|
||||
@JvmName("mapDirectorInformationToEntities")
|
||||
fun List<SdkDirectorInformation>.mapToEntities(student: Student) = map {
|
||||
SchoolAnnouncement(
|
||||
userLoginId = student.userLoginId,
|
||||
date = it.date,
|
||||
subject = it.subject,
|
||||
content = it.content,
|
||||
author = null,
|
||||
)
|
||||
}
|
||||
|
||||
@JvmName("mapLastAnnouncementsToEntities")
|
||||
fun List<SdkLastAnnouncement>.mapToEntities(student: Student) = map {
|
||||
SchoolAnnouncement(
|
||||
userLoginId = student.userLoginId,
|
||||
date = it.date,
|
||||
subject = it.subject,
|
||||
content = it.content,
|
||||
author = it.author,
|
||||
)
|
||||
}
|
||||
|
@ -1,10 +1,12 @@
|
||||
package io.github.wulkanowy.data.mappers
|
||||
|
||||
import io.github.wulkanowy.data.db.entities.Grade
|
||||
import io.github.wulkanowy.data.db.entities.GradeDescriptive
|
||||
import io.github.wulkanowy.data.db.entities.GradeSummary
|
||||
import io.github.wulkanowy.data.db.entities.Semester
|
||||
import io.github.wulkanowy.sdk.pojo.GradeSummary as SdkGradeSummary
|
||||
import io.github.wulkanowy.sdk.pojo.Grade as SdkGrade
|
||||
import io.github.wulkanowy.sdk.pojo.GradeDescriptive as SdkGradeDescriptive
|
||||
import io.github.wulkanowy.sdk.pojo.GradeSummary as SdkGradeSummary
|
||||
|
||||
fun List<SdkGrade>.mapToEntities(semester: Semester) = map {
|
||||
Grade(
|
||||
@ -40,3 +42,15 @@ fun List<SdkGradeSummary>.mapToEntities(semester: Semester) = map {
|
||||
average = it.average
|
||||
)
|
||||
}
|
||||
|
||||
@JvmName("mapGradeDescriptiveToEntities")
|
||||
fun List<SdkGradeDescriptive>.mapToEntities(semester: Semester) = map {
|
||||
GradeDescriptive(
|
||||
semesterId = semester.semesterId,
|
||||
studentId = semester.studentId,
|
||||
subject = it.subject,
|
||||
description = it.description
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
@ -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.networkBoundResource
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.filterNot
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
@ -28,6 +29,6 @@ class AdminMessageRepository @Inject constructor(
|
||||
saveFetchResult = { oldItems, newItems ->
|
||||
adminMessageDao.removeOldAndSaveNew(oldItems, newItems)
|
||||
},
|
||||
showSavedOnLoading = false,
|
||||
)
|
||||
.filterNot { it is Resource.Intermediate }
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
package io.github.wulkanowy.data.repositories
|
||||
|
||||
import io.github.wulkanowy.data.WulkanowySdkFactory
|
||||
import io.github.wulkanowy.data.db.dao.AttendanceDao
|
||||
import io.github.wulkanowy.data.db.dao.TimetableDao
|
||||
import io.github.wulkanowy.data.db.entities.Attendance
|
||||
@ -7,19 +8,14 @@ import io.github.wulkanowy.data.db.entities.Semester
|
||||
import io.github.wulkanowy.data.db.entities.Student
|
||||
import io.github.wulkanowy.data.mappers.mapToEntities
|
||||
import io.github.wulkanowy.data.networkBoundResource
|
||||
import io.github.wulkanowy.sdk.Sdk
|
||||
import io.github.wulkanowy.sdk.pojo.Absent
|
||||
import io.github.wulkanowy.utils.AutoRefreshHelper
|
||||
import io.github.wulkanowy.utils.getRefreshKey
|
||||
import io.github.wulkanowy.utils.init
|
||||
import io.github.wulkanowy.utils.monday
|
||||
import io.github.wulkanowy.utils.sunday
|
||||
import io.github.wulkanowy.utils.switchSemester
|
||||
import io.github.wulkanowy.utils.uniqueSubtract
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalDateTime
|
||||
import java.time.LocalTime
|
||||
@ -30,7 +26,7 @@ import javax.inject.Singleton
|
||||
class AttendanceRepository @Inject constructor(
|
||||
private val attendanceDb: AttendanceDao,
|
||||
private val timetableDb: TimetableDao,
|
||||
private val sdk: Sdk,
|
||||
private val wulkanowySdkFactory: WulkanowySdkFactory,
|
||||
private val refreshHelper: AutoRefreshHelper,
|
||||
) {
|
||||
|
||||
@ -58,23 +54,21 @@ class AttendanceRepository @Inject constructor(
|
||||
attendanceDb.loadAll(semester.diaryId, semester.studentId, start.monday, end.sunday)
|
||||
},
|
||||
fetch = {
|
||||
val lessons = withContext(Dispatchers.IO) {
|
||||
timetableDb.load(
|
||||
semester.diaryId, semester.studentId, start.monday, end.sunday
|
||||
)
|
||||
}
|
||||
sdk.init(student)
|
||||
.switchSemester(semester)
|
||||
val lessons = timetableDb.load(
|
||||
semester.diaryId, semester.studentId, start.monday, end.sunday
|
||||
)
|
||||
wulkanowySdkFactory.create(student, semester)
|
||||
.getAttendance(start.monday, end.sunday)
|
||||
.mapToEntities(semester, lessons)
|
||||
},
|
||||
saveFetchResult = { old, new ->
|
||||
attendanceDb.deleteAll(old uniqueSubtract new)
|
||||
val attendanceToAdd = (new uniqueSubtract old).map { newAttendance ->
|
||||
newAttendance.apply { if (notify) isNotified = false }
|
||||
}
|
||||
attendanceDb.insertAll(attendanceToAdd)
|
||||
|
||||
attendanceDb.removeOldAndSaveNew(
|
||||
oldItems = old uniqueSubtract new,
|
||||
newItems = attendanceToAdd,
|
||||
)
|
||||
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester, start, end))
|
||||
},
|
||||
filterResult = { it.filter { item -> item.date in start..end } }
|
||||
@ -93,8 +87,10 @@ class AttendanceRepository @Inject constructor(
|
||||
}
|
||||
|
||||
suspend fun excuseForAbsence(
|
||||
student: Student, semester: Semester,
|
||||
absenceList: List<Attendance>, reason: String? = null
|
||||
student: Student,
|
||||
semester: Semester,
|
||||
absenceList: List<Attendance>,
|
||||
reason: String? = null
|
||||
) {
|
||||
val items = absenceList.map { attendance ->
|
||||
Absent(
|
||||
@ -102,8 +98,7 @@ class AttendanceRepository @Inject constructor(
|
||||
timeId = attendance.timeId
|
||||
)
|
||||
}
|
||||
sdk.init(student)
|
||||
.switchSemester(semester)
|
||||
wulkanowySdkFactory.create(student, semester)
|
||||
.excuseForAbsence(items, reason)
|
||||
}
|
||||
}
|
||||
|
@ -1,15 +1,15 @@
|
||||
package io.github.wulkanowy.data.repositories
|
||||
|
||||
import androidx.room.withTransaction
|
||||
import io.github.wulkanowy.data.WulkanowySdkFactory
|
||||
import io.github.wulkanowy.data.db.AppDatabase
|
||||
import io.github.wulkanowy.data.db.dao.AttendanceSummaryDao
|
||||
import io.github.wulkanowy.data.db.entities.Semester
|
||||
import io.github.wulkanowy.data.db.entities.Student
|
||||
import io.github.wulkanowy.data.mappers.mapToEntities
|
||||
import io.github.wulkanowy.data.networkBoundResource
|
||||
import io.github.wulkanowy.sdk.Sdk
|
||||
import io.github.wulkanowy.utils.AutoRefreshHelper
|
||||
import io.github.wulkanowy.utils.getRefreshKey
|
||||
import io.github.wulkanowy.utils.init
|
||||
import io.github.wulkanowy.utils.switchSemester
|
||||
import io.github.wulkanowy.utils.uniqueSubtract
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import javax.inject.Inject
|
||||
@ -18,8 +18,9 @@ import javax.inject.Singleton
|
||||
@Singleton
|
||||
class AttendanceSummaryRepository @Inject constructor(
|
||||
private val attendanceDb: AttendanceSummaryDao,
|
||||
private val sdk: Sdk,
|
||||
private val refreshHelper: AutoRefreshHelper,
|
||||
private val appDatabase: AppDatabase,
|
||||
private val wulkanowySdkFactory: WulkanowySdkFactory,
|
||||
) {
|
||||
|
||||
private val saveFetchResultMutex = Mutex()
|
||||
@ -40,14 +41,15 @@ class AttendanceSummaryRepository @Inject constructor(
|
||||
},
|
||||
query = { attendanceDb.loadAll(semester.diaryId, semester.studentId, subjectId) },
|
||||
fetch = {
|
||||
sdk.init(student)
|
||||
.switchSemester(semester)
|
||||
wulkanowySdkFactory.create(student, semester)
|
||||
.getAttendanceSummary(subjectId)
|
||||
.mapToEntities(semester, subjectId)
|
||||
},
|
||||
saveFetchResult = { old, new ->
|
||||
attendanceDb.deleteAll(old uniqueSubtract new)
|
||||
attendanceDb.insertAll(new uniqueSubtract old)
|
||||
appDatabase.withTransaction {
|
||||
attendanceDb.deleteAll(old uniqueSubtract new)
|
||||
attendanceDb.insertAll(new uniqueSubtract old)
|
||||
}
|
||||
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester))
|
||||
}
|
||||
)
|
||||
|
@ -1,12 +1,16 @@
|
||||
package io.github.wulkanowy.data.repositories
|
||||
|
||||
import io.github.wulkanowy.data.WulkanowySdkFactory
|
||||
import io.github.wulkanowy.data.db.dao.CompletedLessonsDao
|
||||
import io.github.wulkanowy.data.db.entities.Semester
|
||||
import io.github.wulkanowy.data.db.entities.Student
|
||||
import io.github.wulkanowy.data.mappers.mapToEntities
|
||||
import io.github.wulkanowy.data.networkBoundResource
|
||||
import io.github.wulkanowy.sdk.Sdk
|
||||
import io.github.wulkanowy.utils.*
|
||||
import io.github.wulkanowy.utils.AutoRefreshHelper
|
||||
import io.github.wulkanowy.utils.getRefreshKey
|
||||
import io.github.wulkanowy.utils.monday
|
||||
import io.github.wulkanowy.utils.sunday
|
||||
import io.github.wulkanowy.utils.uniqueSubtract
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import java.time.LocalDate
|
||||
import javax.inject.Inject
|
||||
@ -15,7 +19,7 @@ import javax.inject.Singleton
|
||||
@Singleton
|
||||
class CompletedLessonsRepository @Inject constructor(
|
||||
private val completedLessonsDb: CompletedLessonsDao,
|
||||
private val sdk: Sdk,
|
||||
private val wulkanowySdkFactory: WulkanowySdkFactory,
|
||||
private val refreshHelper: AutoRefreshHelper,
|
||||
) {
|
||||
|
||||
@ -47,14 +51,15 @@ class CompletedLessonsRepository @Inject constructor(
|
||||
)
|
||||
},
|
||||
fetch = {
|
||||
sdk.init(student)
|
||||
.switchSemester(semester)
|
||||
wulkanowySdkFactory.create(student, semester)
|
||||
.getCompletedLessons(start.monday, end.sunday)
|
||||
.mapToEntities(semester)
|
||||
},
|
||||
saveFetchResult = { old, new ->
|
||||
completedLessonsDb.deleteAll(old uniqueSubtract new)
|
||||
completedLessonsDb.insertAll(new uniqueSubtract old)
|
||||
completedLessonsDb.removeOldAndSaveNew(
|
||||
oldItems = old uniqueSubtract new,
|
||||
newItems = new uniqueSubtract old,
|
||||
)
|
||||
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester, start, end))
|
||||
},
|
||||
filterResult = { it.filter { item -> item.date in start..end } }
|
||||
|
@ -1,16 +1,14 @@
|
||||
package io.github.wulkanowy.data.repositories
|
||||
|
||||
import io.github.wulkanowy.data.WulkanowySdkFactory
|
||||
import io.github.wulkanowy.data.db.dao.ConferenceDao
|
||||
import io.github.wulkanowy.data.db.entities.Conference
|
||||
import io.github.wulkanowy.data.db.entities.Semester
|
||||
import io.github.wulkanowy.data.db.entities.Student
|
||||
import io.github.wulkanowy.data.mappers.mapToEntities
|
||||
import io.github.wulkanowy.data.networkBoundResource
|
||||
import io.github.wulkanowy.sdk.Sdk
|
||||
import io.github.wulkanowy.utils.AutoRefreshHelper
|
||||
import io.github.wulkanowy.utils.getRefreshKey
|
||||
import io.github.wulkanowy.utils.init
|
||||
import io.github.wulkanowy.utils.switchSemester
|
||||
import io.github.wulkanowy.utils.uniqueSubtract
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
@ -21,7 +19,7 @@ import javax.inject.Singleton
|
||||
@Singleton
|
||||
class ConferenceRepository @Inject constructor(
|
||||
private val conferenceDb: ConferenceDao,
|
||||
private val sdk: Sdk,
|
||||
private val wulkanowySdkFactory: WulkanowySdkFactory,
|
||||
private val refreshHelper: AutoRefreshHelper,
|
||||
) {
|
||||
|
||||
@ -46,19 +44,18 @@ class ConferenceRepository @Inject constructor(
|
||||
conferenceDb.loadAll(semester.diaryId, student.studentId, startDate)
|
||||
},
|
||||
fetch = {
|
||||
sdk.init(student)
|
||||
.switchSemester(semester)
|
||||
wulkanowySdkFactory.create(student, semester)
|
||||
.getConferences()
|
||||
.mapToEntities(semester)
|
||||
.filter { it.date >= startDate }
|
||||
},
|
||||
saveFetchResult = { old, new ->
|
||||
val conferencesToSave = (new uniqueSubtract old).onEach {
|
||||
if (notify) it.isNotified = false
|
||||
}
|
||||
|
||||
conferenceDb.deleteAll(old uniqueSubtract new)
|
||||
conferenceDb.insertAll(conferencesToSave)
|
||||
conferenceDb.removeOldAndSaveNew(
|
||||
oldItems = old uniqueSubtract new,
|
||||
newItems = (new uniqueSubtract old).onEach {
|
||||
if (notify) it.isNotified = false
|
||||
},
|
||||
)
|
||||
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester))
|
||||
}
|
||||
)
|
||||
|
@ -1,18 +1,16 @@
|
||||
package io.github.wulkanowy.data.repositories
|
||||
|
||||
import io.github.wulkanowy.data.WulkanowySdkFactory
|
||||
import io.github.wulkanowy.data.db.dao.ExamDao
|
||||
import io.github.wulkanowy.data.db.entities.Exam
|
||||
import io.github.wulkanowy.data.db.entities.Semester
|
||||
import io.github.wulkanowy.data.db.entities.Student
|
||||
import io.github.wulkanowy.data.mappers.mapToEntities
|
||||
import io.github.wulkanowy.data.networkBoundResource
|
||||
import io.github.wulkanowy.sdk.Sdk
|
||||
import io.github.wulkanowy.utils.AutoRefreshHelper
|
||||
import io.github.wulkanowy.utils.endExamsDay
|
||||
import io.github.wulkanowy.utils.getRefreshKey
|
||||
import io.github.wulkanowy.utils.init
|
||||
import io.github.wulkanowy.utils.startExamsDay
|
||||
import io.github.wulkanowy.utils.switchSemester
|
||||
import io.github.wulkanowy.utils.uniqueSubtract
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
@ -23,7 +21,7 @@ import javax.inject.Singleton
|
||||
@Singleton
|
||||
class ExamRepository @Inject constructor(
|
||||
private val examDb: ExamDao,
|
||||
private val sdk: Sdk,
|
||||
private val wulkanowySdkFactory: WulkanowySdkFactory,
|
||||
private val refreshHelper: AutoRefreshHelper,
|
||||
) {
|
||||
|
||||
@ -56,18 +54,17 @@ class ExamRepository @Inject constructor(
|
||||
)
|
||||
},
|
||||
fetch = {
|
||||
sdk.init(student)
|
||||
.switchSemester(semester)
|
||||
wulkanowySdkFactory.create(student, semester)
|
||||
.getExams(start.startExamsDay, start.endExamsDay)
|
||||
.mapToEntities(semester)
|
||||
},
|
||||
saveFetchResult = { old, new ->
|
||||
val examsToSave = (new uniqueSubtract old).onEach {
|
||||
if (notify) it.isNotified = false
|
||||
}
|
||||
|
||||
examDb.deleteAll(old uniqueSubtract new)
|
||||
examDb.insertAll(examsToSave)
|
||||
examDb.removeOldAndSaveNew(
|
||||
oldItems = old uniqueSubtract new,
|
||||
newItems = (new uniqueSubtract old).onEach {
|
||||
if (notify) it.isNotified = false
|
||||
},
|
||||
)
|
||||
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester, start, end))
|
||||
},
|
||||
filterResult = { it.filter { item -> item.date in start..end } }
|
||||
|
@ -1,15 +1,20 @@
|
||||
package io.github.wulkanowy.data.repositories
|
||||
|
||||
import io.github.wulkanowy.data.WulkanowySdkFactory
|
||||
import io.github.wulkanowy.data.db.dao.GradeDao
|
||||
import io.github.wulkanowy.data.db.dao.GradeDescriptiveDao
|
||||
import io.github.wulkanowy.data.db.dao.GradeSummaryDao
|
||||
import io.github.wulkanowy.data.db.entities.Grade
|
||||
import io.github.wulkanowy.data.db.entities.GradeDescriptive
|
||||
import io.github.wulkanowy.data.db.entities.GradeSummary
|
||||
import io.github.wulkanowy.data.db.entities.Semester
|
||||
import io.github.wulkanowy.data.db.entities.Student
|
||||
import io.github.wulkanowy.data.mappers.mapToEntities
|
||||
import io.github.wulkanowy.data.networkBoundResource
|
||||
import io.github.wulkanowy.sdk.Sdk
|
||||
import io.github.wulkanowy.utils.*
|
||||
import io.github.wulkanowy.utils.AutoRefreshHelper
|
||||
import io.github.wulkanowy.utils.getRefreshKey
|
||||
import io.github.wulkanowy.utils.toLocalDate
|
||||
import io.github.wulkanowy.utils.uniqueSubtract
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.map
|
||||
@ -22,14 +27,13 @@ import javax.inject.Singleton
|
||||
class GradeRepository @Inject constructor(
|
||||
private val gradeDb: GradeDao,
|
||||
private val gradeSummaryDb: GradeSummaryDao,
|
||||
private val sdk: Sdk,
|
||||
private val gradeDescriptiveDb: GradeDescriptiveDao,
|
||||
private val wulkanowySdkFactory: WulkanowySdkFactory,
|
||||
private val refreshHelper: AutoRefreshHelper,
|
||||
) {
|
||||
|
||||
private val saveFetchResultMutex = Mutex()
|
||||
|
||||
private val cacheKey = "grade"
|
||||
|
||||
fun getGrades(
|
||||
student: Student,
|
||||
semester: Semester,
|
||||
@ -41,30 +45,53 @@ class GradeRepository @Inject constructor(
|
||||
//When details is empty and summary is not, app will not use summary cache - edge case
|
||||
it.first.isEmpty()
|
||||
},
|
||||
shouldFetch = { (details, summaries) ->
|
||||
val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, semester))
|
||||
details.isEmpty() || summaries.isEmpty() || forceRefresh || isExpired
|
||||
shouldFetch = { (details, summaries, descriptive) ->
|
||||
val isExpired =
|
||||
refreshHelper.shouldBeRefreshed(getRefreshKey(GRADE_CACHE_KEY, semester))
|
||||
details.isEmpty() || (summaries.isEmpty() && descriptive.isEmpty()) || forceRefresh || isExpired
|
||||
},
|
||||
query = {
|
||||
val detailsFlow = gradeDb.loadAll(semester.semesterId, semester.studentId)
|
||||
val summaryFlow = gradeSummaryDb.loadAll(semester.semesterId, semester.studentId)
|
||||
detailsFlow.combine(summaryFlow) { details, summaries -> details to summaries }
|
||||
val descriptiveFlow =
|
||||
gradeDescriptiveDb.loadAll(semester.semesterId, semester.studentId)
|
||||
|
||||
combine(detailsFlow, summaryFlow, descriptiveFlow) { details, summaries, descriptive ->
|
||||
Triple(details, summaries, descriptive)
|
||||
}
|
||||
},
|
||||
fetch = {
|
||||
val (details, summary) = sdk.init(student)
|
||||
.switchSemester(semester)
|
||||
val (details, summary, descriptive) = wulkanowySdkFactory.create(student, semester)
|
||||
.getGrades(semester.semesterId)
|
||||
|
||||
details.mapToEntities(semester) to summary.mapToEntities(semester)
|
||||
Triple(
|
||||
details.mapToEntities(semester),
|
||||
summary.mapToEntities(semester),
|
||||
descriptive.mapToEntities(semester)
|
||||
)
|
||||
},
|
||||
saveFetchResult = { (oldDetails, oldSummary), (newDetails, newSummary) ->
|
||||
saveFetchResult = { (oldDetails, oldSummary, oldDescriptive), (newDetails, newSummary, newDescriptive) ->
|
||||
refreshGradeDetails(student, oldDetails, newDetails, notify)
|
||||
refreshGradeSummaries(oldSummary, newSummary, notify)
|
||||
refreshGradeDescriptions(oldDescriptive, newDescriptive, notify)
|
||||
|
||||
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester))
|
||||
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(GRADE_CACHE_KEY, semester))
|
||||
}
|
||||
)
|
||||
|
||||
private suspend fun refreshGradeDescriptions(
|
||||
old: List<GradeDescriptive>,
|
||||
new: List<GradeDescriptive>,
|
||||
notify: Boolean
|
||||
) {
|
||||
gradeDescriptiveDb.removeOldAndSaveNew(
|
||||
oldItems = old uniqueSubtract new,
|
||||
newItems = (new uniqueSubtract old).onEach {
|
||||
if (notify) it.isNotified = false
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun refreshGradeDetails(
|
||||
student: Student,
|
||||
oldGrades: List<Grade>,
|
||||
@ -73,13 +100,16 @@ class GradeRepository @Inject constructor(
|
||||
) {
|
||||
val notifyBreakDate = oldGrades.maxByOrNull { it.date }?.date
|
||||
?: student.registrationDate.toLocalDate()
|
||||
gradeDb.deleteAll(oldGrades uniqueSubtract newDetails)
|
||||
gradeDb.insertAll((newDetails uniqueSubtract oldGrades).onEach {
|
||||
if (it.date >= notifyBreakDate) it.apply {
|
||||
isRead = false
|
||||
if (notify) isNotified = false
|
||||
}
|
||||
})
|
||||
|
||||
gradeDb.removeOldAndSaveNew(
|
||||
oldItems = oldGrades uniqueSubtract newDetails,
|
||||
newItems = (newDetails uniqueSubtract oldGrades).onEach {
|
||||
if (it.date >= notifyBreakDate) it.apply {
|
||||
isRead = false
|
||||
if (notify) isNotified = false
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun refreshGradeSummaries(
|
||||
@ -87,31 +117,43 @@ class GradeRepository @Inject constructor(
|
||||
newSummary: List<GradeSummary>,
|
||||
notify: Boolean
|
||||
) {
|
||||
gradeSummaryDb.deleteAll(oldSummaries uniqueSubtract newSummary)
|
||||
gradeSummaryDb.insertAll((newSummary uniqueSubtract oldSummaries).onEach { summary ->
|
||||
val oldSummary = oldSummaries.find { old -> old.subject == summary.subject }
|
||||
summary.isPredictedGradeNotified = when {
|
||||
summary.predictedGrade.isEmpty() -> true
|
||||
notify && oldSummary?.predictedGrade != summary.predictedGrade -> false
|
||||
else -> true
|
||||
}
|
||||
summary.isFinalGradeNotified = when {
|
||||
summary.finalGrade.isEmpty() -> true
|
||||
notify && oldSummary?.finalGrade != summary.finalGrade -> false
|
||||
else -> true
|
||||
}
|
||||
gradeSummaryDb.removeOldAndSaveNew(
|
||||
oldItems = oldSummaries uniqueSubtract newSummary,
|
||||
newItems = (newSummary uniqueSubtract oldSummaries).onEach { summary ->
|
||||
getGradeSummaryWithUpdatedNotificationState(
|
||||
summary = summary,
|
||||
oldSummary = oldSummaries.find { it.subject == summary.subject },
|
||||
notify = notify,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
summary.predictedGradeLastChange = when {
|
||||
oldSummary == null -> Instant.now()
|
||||
summary.predictedGrade != oldSummary.predictedGrade -> Instant.now()
|
||||
else -> oldSummary.predictedGradeLastChange
|
||||
}
|
||||
summary.finalGradeLastChange = when {
|
||||
oldSummary == null -> Instant.now()
|
||||
summary.finalGrade != oldSummary.finalGrade -> Instant.now()
|
||||
else -> oldSummary.finalGradeLastChange
|
||||
}
|
||||
})
|
||||
private fun getGradeSummaryWithUpdatedNotificationState(
|
||||
summary: GradeSummary,
|
||||
oldSummary: GradeSummary?,
|
||||
notify: Boolean,
|
||||
) {
|
||||
summary.isPredictedGradeNotified = when {
|
||||
summary.predictedGrade.isEmpty() -> true
|
||||
notify && oldSummary?.predictedGrade != summary.predictedGrade -> false
|
||||
else -> true
|
||||
}
|
||||
summary.isFinalGradeNotified = when {
|
||||
summary.finalGrade.isEmpty() -> true
|
||||
notify && oldSummary?.finalGrade != summary.finalGrade -> false
|
||||
else -> true
|
||||
}
|
||||
summary.predictedGradeLastChange = when {
|
||||
oldSummary == null -> Instant.now()
|
||||
summary.predictedGrade != oldSummary.predictedGrade -> Instant.now()
|
||||
else -> oldSummary.predictedGradeLastChange
|
||||
}
|
||||
summary.finalGradeLastChange = when {
|
||||
oldSummary == null -> Instant.now()
|
||||
summary.finalGrade != oldSummary.finalGrade -> Instant.now()
|
||||
else -> oldSummary.finalGradeLastChange
|
||||
}
|
||||
}
|
||||
|
||||
fun getUnreadGrades(semester: Semester): Flow<List<Grade>> {
|
||||
@ -132,6 +174,10 @@ class GradeRepository @Inject constructor(
|
||||
return gradeSummaryDb.loadAll(semester.semesterId, semester.studentId)
|
||||
}
|
||||
|
||||
fun getGradesDescriptiveFromDatabase(semester: Semester): Flow<List<GradeDescriptive>> {
|
||||
return gradeDescriptiveDb.loadAll(semester.semesterId, semester.studentId)
|
||||
}
|
||||
|
||||
suspend fun updateGrade(grade: Grade) {
|
||||
return gradeDb.updateAll(listOf(grade))
|
||||
}
|
||||
@ -143,4 +189,13 @@ class GradeRepository @Inject constructor(
|
||||
suspend fun updateGradesSummary(gradesSummary: List<GradeSummary>) {
|
||||
return gradeSummaryDb.updateAll(gradesSummary)
|
||||
}
|
||||
|
||||
suspend fun updateGradesDescriptive(gradesDescriptive: List<GradeDescriptive>) {
|
||||
return gradeDescriptiveDb.updateAll(gradesDescriptive)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
private const val GRADE_CACHE_KEY = "grade"
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
package io.github.wulkanowy.data.repositories
|
||||
|
||||
import io.github.wulkanowy.data.WulkanowySdkFactory
|
||||
import io.github.wulkanowy.data.db.dao.GradePartialStatisticsDao
|
||||
import io.github.wulkanowy.data.db.dao.GradePointsStatisticsDao
|
||||
import io.github.wulkanowy.data.db.dao.GradeSemesterStatisticsDao
|
||||
@ -12,14 +13,11 @@ import io.github.wulkanowy.data.mappers.mapPointsToStatisticsItems
|
||||
import io.github.wulkanowy.data.mappers.mapSemesterToStatisticItems
|
||||
import io.github.wulkanowy.data.mappers.mapToEntities
|
||||
import io.github.wulkanowy.data.networkBoundResource
|
||||
import io.github.wulkanowy.sdk.Sdk
|
||||
import io.github.wulkanowy.utils.AutoRefreshHelper
|
||||
import io.github.wulkanowy.utils.getRefreshKey
|
||||
import io.github.wulkanowy.utils.init
|
||||
import io.github.wulkanowy.utils.switchSemester
|
||||
import io.github.wulkanowy.utils.uniqueSubtract
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import java.util.*
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@ -28,7 +26,7 @@ class GradeStatisticsRepository @Inject constructor(
|
||||
private val gradePartialStatisticsDb: GradePartialStatisticsDao,
|
||||
private val gradePointsStatisticsDb: GradePointsStatisticsDao,
|
||||
private val gradeSemesterStatisticsDb: GradeSemesterStatisticsDao,
|
||||
private val sdk: Sdk,
|
||||
private val wulkanowySdkFactory: WulkanowySdkFactory,
|
||||
private val refreshHelper: AutoRefreshHelper,
|
||||
) {
|
||||
|
||||
@ -56,14 +54,15 @@ class GradeStatisticsRepository @Inject constructor(
|
||||
},
|
||||
query = { gradePartialStatisticsDb.loadAll(semester.semesterId, semester.studentId) },
|
||||
fetch = {
|
||||
sdk.init(student)
|
||||
.switchSemester(semester)
|
||||
wulkanowySdkFactory.create(student, semester)
|
||||
.getGradesPartialStatistics(semester.semesterId)
|
||||
.mapToEntities(semester)
|
||||
},
|
||||
saveFetchResult = { old, new ->
|
||||
gradePartialStatisticsDb.deleteAll(old uniqueSubtract new)
|
||||
gradePartialStatisticsDb.insertAll(new uniqueSubtract old)
|
||||
gradePartialStatisticsDb.removeOldAndSaveNew(
|
||||
oldItems = old uniqueSubtract new,
|
||||
newItems = new uniqueSubtract old,
|
||||
)
|
||||
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(partialCacheKey, semester))
|
||||
},
|
||||
mapResult = { items ->
|
||||
@ -80,6 +79,7 @@ class GradeStatisticsRepository @Inject constructor(
|
||||
)
|
||||
listOf(summaryItem) + items
|
||||
}
|
||||
|
||||
else -> items.filter { it.subject == subjectName }
|
||||
}.mapPartialToStatisticItems()
|
||||
}
|
||||
@ -101,14 +101,15 @@ class GradeStatisticsRepository @Inject constructor(
|
||||
},
|
||||
query = { gradeSemesterStatisticsDb.loadAll(semester.semesterId, semester.studentId) },
|
||||
fetch = {
|
||||
sdk.init(student)
|
||||
.switchSemester(semester)
|
||||
wulkanowySdkFactory.create(student, semester)
|
||||
.getGradesSemesterStatistics(semester.semesterId)
|
||||
.mapToEntities(semester)
|
||||
},
|
||||
saveFetchResult = { old, new ->
|
||||
gradeSemesterStatisticsDb.deleteAll(old uniqueSubtract new)
|
||||
gradeSemesterStatisticsDb.insertAll(new uniqueSubtract old)
|
||||
gradeSemesterStatisticsDb.removeOldAndSaveNew(
|
||||
oldItems = old uniqueSubtract new,
|
||||
newItems = new uniqueSubtract old,
|
||||
)
|
||||
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(semesterCacheKey, semester))
|
||||
},
|
||||
mapResult = { items ->
|
||||
@ -138,6 +139,7 @@ class GradeStatisticsRepository @Inject constructor(
|
||||
}
|
||||
listOf(summaryItem) + itemsWithAverage
|
||||
}
|
||||
|
||||
else -> itemsWithAverage.filter { it.subject == subjectName }
|
||||
}.mapSemesterToStatisticItems()
|
||||
}
|
||||
@ -157,14 +159,15 @@ class GradeStatisticsRepository @Inject constructor(
|
||||
},
|
||||
query = { gradePointsStatisticsDb.loadAll(semester.semesterId, semester.studentId) },
|
||||
fetch = {
|
||||
sdk.init(student)
|
||||
.switchSemester(semester)
|
||||
wulkanowySdkFactory.create(student, semester)
|
||||
.getGradesPointsStatistics(semester.semesterId)
|
||||
.mapToEntities(semester)
|
||||
},
|
||||
saveFetchResult = { old, new ->
|
||||
gradePointsStatisticsDb.deleteAll(old uniqueSubtract new)
|
||||
gradePointsStatisticsDb.insertAll(new uniqueSubtract old)
|
||||
gradePointsStatisticsDb.removeOldAndSaveNew(
|
||||
oldItems = old uniqueSubtract new,
|
||||
newItems = new uniqueSubtract old,
|
||||
)
|
||||
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(pointsCacheKey, semester))
|
||||
},
|
||||
mapResult = { items ->
|
||||
|
@ -1,18 +1,16 @@
|
||||
package io.github.wulkanowy.data.repositories
|
||||
|
||||
import io.github.wulkanowy.data.WulkanowySdkFactory
|
||||
import io.github.wulkanowy.data.db.dao.HomeworkDao
|
||||
import io.github.wulkanowy.data.db.entities.Homework
|
||||
import io.github.wulkanowy.data.db.entities.Semester
|
||||
import io.github.wulkanowy.data.db.entities.Student
|
||||
import io.github.wulkanowy.data.mappers.mapToEntities
|
||||
import io.github.wulkanowy.data.networkBoundResource
|
||||
import io.github.wulkanowy.sdk.Sdk
|
||||
import io.github.wulkanowy.utils.AutoRefreshHelper
|
||||
import io.github.wulkanowy.utils.getRefreshKey
|
||||
import io.github.wulkanowy.utils.init
|
||||
import io.github.wulkanowy.utils.monday
|
||||
import io.github.wulkanowy.utils.sunday
|
||||
import io.github.wulkanowy.utils.switchSemester
|
||||
import io.github.wulkanowy.utils.uniqueSubtract
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import java.time.LocalDate
|
||||
@ -22,7 +20,7 @@ import javax.inject.Singleton
|
||||
@Singleton
|
||||
class HomeworkRepository @Inject constructor(
|
||||
private val homeworkDb: HomeworkDao,
|
||||
private val sdk: Sdk,
|
||||
private val wulkanowySdkFactory: WulkanowySdkFactory,
|
||||
private val refreshHelper: AutoRefreshHelper,
|
||||
) {
|
||||
|
||||
@ -55,20 +53,19 @@ class HomeworkRepository @Inject constructor(
|
||||
)
|
||||
},
|
||||
fetch = {
|
||||
sdk.init(student)
|
||||
.switchSemester(semester)
|
||||
wulkanowySdkFactory.create(student, semester)
|
||||
.getHomework(start.monday, end.sunday)
|
||||
.mapToEntities(semester)
|
||||
},
|
||||
saveFetchResult = { old, new ->
|
||||
val homeWorkToSave = (new uniqueSubtract old).onEach {
|
||||
if (notify) it.isNotified = false
|
||||
}
|
||||
val filteredOld = old.filterNot { it.isAddedByUser }
|
||||
|
||||
homeworkDb.deleteAll(filteredOld uniqueSubtract new)
|
||||
homeworkDb.insertAll(homeWorkToSave)
|
||||
|
||||
homeworkDb.removeOldAndSaveNew(
|
||||
oldItems = filteredOld uniqueSubtract new,
|
||||
newItems = (new uniqueSubtract old).onEach {
|
||||
if (notify) it.isNotified = false
|
||||
},
|
||||
)
|
||||
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester, start, end))
|
||||
}
|
||||
)
|
||||
|
@ -1,12 +1,11 @@
|
||||
package io.github.wulkanowy.data.repositories
|
||||
|
||||
import io.github.wulkanowy.data.WulkanowySdkFactory
|
||||
import io.github.wulkanowy.data.db.dao.LuckyNumberDao
|
||||
import io.github.wulkanowy.data.db.entities.LuckyNumber
|
||||
import io.github.wulkanowy.data.db.entities.Student
|
||||
import io.github.wulkanowy.data.mappers.mapToEntity
|
||||
import io.github.wulkanowy.data.networkBoundResource
|
||||
import io.github.wulkanowy.sdk.Sdk
|
||||
import io.github.wulkanowy.utils.init
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
@ -18,7 +17,7 @@ import javax.inject.Singleton
|
||||
@Singleton
|
||||
class LuckyNumberRepository @Inject constructor(
|
||||
private val luckyNumberDb: LuckyNumberDao,
|
||||
private val sdk: Sdk
|
||||
private val wulkanowySdkFactory: WulkanowySdkFactory,
|
||||
) {
|
||||
|
||||
private val saveFetchResultMutex = Mutex()
|
||||
@ -33,17 +32,18 @@ class LuckyNumberRepository @Inject constructor(
|
||||
shouldFetch = { it == null || forceRefresh },
|
||||
query = { luckyNumberDb.load(student.studentId, now()) },
|
||||
fetch = {
|
||||
sdk.init(student).getLuckyNumber(student.schoolShortName)?.mapToEntity(student)
|
||||
wulkanowySdkFactory.create(student)
|
||||
.getLuckyNumber(student.schoolShortName)
|
||||
?.mapToEntity(student)
|
||||
},
|
||||
saveFetchResult = { oldLuckyNumber, newLuckyNumber ->
|
||||
newLuckyNumber ?: return@networkBoundResource
|
||||
|
||||
if (newLuckyNumber != oldLuckyNumber) {
|
||||
val updatedLuckNumberList =
|
||||
listOf(newLuckyNumber.apply { if (notify) isNotified = false })
|
||||
|
||||
oldLuckyNumber?.let { luckyNumberDb.deleteAll(listOfNotNull(it)) }
|
||||
luckyNumberDb.insertAll(updatedLuckNumberList)
|
||||
luckyNumberDb.removeOldAndSaveNew(
|
||||
oldItems = listOfNotNull(oldLuckyNumber),
|
||||
newItems = listOf(newLuckyNumber.apply { if (notify) isNotified = false }),
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
@ -4,17 +4,22 @@ import android.content.Context
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import io.github.wulkanowy.R
|
||||
import io.github.wulkanowy.data.Resource
|
||||
import io.github.wulkanowy.data.WulkanowySdkFactory
|
||||
import io.github.wulkanowy.data.db.SharedPrefProvider
|
||||
import io.github.wulkanowy.data.db.dao.MailboxDao
|
||||
import io.github.wulkanowy.data.db.dao.MessageAttachmentDao
|
||||
import io.github.wulkanowy.data.db.dao.MessagesDao
|
||||
import io.github.wulkanowy.data.db.dao.MutedMessageSendersDao
|
||||
import io.github.wulkanowy.data.db.entities.Mailbox
|
||||
import io.github.wulkanowy.data.db.entities.Message
|
||||
import io.github.wulkanowy.data.db.entities.MessageWithAttachment
|
||||
import io.github.wulkanowy.data.db.entities.MessageWithMutedAuthor
|
||||
import io.github.wulkanowy.data.db.entities.MutedMessageSender
|
||||
import io.github.wulkanowy.data.db.entities.Recipient
|
||||
import io.github.wulkanowy.data.db.entities.Student
|
||||
import io.github.wulkanowy.data.enums.MessageFolder
|
||||
import io.github.wulkanowy.data.enums.MessageFolder.RECEIVED
|
||||
import io.github.wulkanowy.data.enums.MessageFolder.SENT
|
||||
import io.github.wulkanowy.data.enums.MessageFolder.TRASHED
|
||||
import io.github.wulkanowy.data.mappers.mapFromEntities
|
||||
import io.github.wulkanowy.data.mappers.mapToEntities
|
||||
@ -22,16 +27,14 @@ import io.github.wulkanowy.data.networkBoundResource
|
||||
import io.github.wulkanowy.data.onResourceError
|
||||
import io.github.wulkanowy.data.onResourceSuccess
|
||||
import io.github.wulkanowy.data.pojos.MessageDraft
|
||||
import io.github.wulkanowy.data.toFirstResult
|
||||
import io.github.wulkanowy.data.waitForResult
|
||||
import io.github.wulkanowy.domain.messages.GetMailboxByStudentUseCase
|
||||
import io.github.wulkanowy.sdk.Sdk
|
||||
import io.github.wulkanowy.sdk.pojo.Folder
|
||||
import io.github.wulkanowy.utils.AutoRefreshHelper
|
||||
import io.github.wulkanowy.utils.getRefreshKey
|
||||
import io.github.wulkanowy.utils.init
|
||||
import io.github.wulkanowy.utils.uniqueSubtract
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
@ -42,8 +45,9 @@ import javax.inject.Singleton
|
||||
@Singleton
|
||||
class MessageRepository @Inject constructor(
|
||||
private val messagesDb: MessagesDao,
|
||||
private val mutedMessageSendersDao: MutedMessageSendersDao,
|
||||
private val messageAttachmentDao: MessageAttachmentDao,
|
||||
private val sdk: Sdk,
|
||||
private val wulkanowySdkFactory: WulkanowySdkFactory,
|
||||
@ApplicationContext private val context: Context,
|
||||
private val refreshHelper: AutoRefreshHelper,
|
||||
private val sharedPrefProvider: SharedPrefProvider,
|
||||
@ -51,7 +55,6 @@ class MessageRepository @Inject constructor(
|
||||
private val mailboxDao: MailboxDao,
|
||||
private val getMailboxByStudentUseCase: GetMailboxByStudentUseCase,
|
||||
) {
|
||||
|
||||
private val saveFetchResultMutex = Mutex()
|
||||
|
||||
private val messagesCacheKey = "message"
|
||||
@ -63,7 +66,7 @@ class MessageRepository @Inject constructor(
|
||||
folder: MessageFolder,
|
||||
forceRefresh: Boolean,
|
||||
notify: Boolean = false,
|
||||
): Flow<Resource<List<Message>>> = networkBoundResource(
|
||||
): Flow<Resource<List<MessageWithMutedAuthor>>> = networkBoundResource(
|
||||
mutex = saveFetchResultMutex,
|
||||
isResultEmpty = { it.isEmpty() },
|
||||
shouldFetch = {
|
||||
@ -74,21 +77,30 @@ class MessageRepository @Inject constructor(
|
||||
},
|
||||
query = {
|
||||
if (mailbox == null) {
|
||||
messagesDb.loadAll(folder.id, student.email)
|
||||
} else messagesDb.loadAll(mailbox.globalKey, folder.id)
|
||||
messagesDb.loadMessagesWithMutedAuthor(folder.id, student.email)
|
||||
} else messagesDb.loadMessagesWithMutedAuthor(mailbox.globalKey, folder.id)
|
||||
},
|
||||
fetch = {
|
||||
sdk.init(student).getMessages(
|
||||
folder = Folder.valueOf(folder.name),
|
||||
mailboxKey = mailbox?.globalKey,
|
||||
).mapToEntities(student, mailbox, mailboxDao.loadAll(student.email))
|
||||
wulkanowySdkFactory.create(student)
|
||||
.getMessages(
|
||||
folder = Folder.valueOf(folder.name),
|
||||
mailboxKey = mailbox?.globalKey,
|
||||
)
|
||||
.mapToEntities(
|
||||
student = student,
|
||||
mailbox = mailbox,
|
||||
allMailboxes = mailboxDao.loadAll(student.email)
|
||||
)
|
||||
},
|
||||
saveFetchResult = { old, new ->
|
||||
messagesDb.deleteAll(old uniqueSubtract new)
|
||||
messagesDb.insertAll((new uniqueSubtract old).onEach {
|
||||
it.isNotified = !notify
|
||||
})
|
||||
|
||||
saveFetchResult = { oldWithAuthors, new ->
|
||||
val old = oldWithAuthors.map { it.message }
|
||||
messagesDb.removeOldAndSaveNew(
|
||||
oldItems = old uniqueSubtract new,
|
||||
newItems = (new uniqueSubtract old).onEach {
|
||||
val muted = isMuted(it.correspondents)
|
||||
it.isNotified = !notify || muted
|
||||
},
|
||||
)
|
||||
refreshHelper.updateLastRefreshTimestamp(
|
||||
getRefreshKey(messagesCacheKey, mailbox, folder)
|
||||
)
|
||||
@ -106,14 +118,13 @@ class MessageRepository @Inject constructor(
|
||||
Timber.d("Message content in db empty: ${it.message.content.isBlank()}")
|
||||
(it.message.unread && markAsRead) || it.message.content.isBlank()
|
||||
},
|
||||
query = {
|
||||
messagesDb.loadMessageWithAttachment(message.messageGlobalKey)
|
||||
},
|
||||
query = { messagesDb.loadMessageWithAttachment(message.messageGlobalKey) },
|
||||
fetch = {
|
||||
sdk.init(student).getMessageDetails(
|
||||
messageKey = it!!.message.messageGlobalKey,
|
||||
markAsRead = message.unread && markAsRead,
|
||||
)
|
||||
wulkanowySdkFactory.create(student)
|
||||
.getMessageDetails(
|
||||
messageKey = message.messageGlobalKey,
|
||||
markAsRead = message.unread && markAsRead,
|
||||
)
|
||||
},
|
||||
saveFetchResult = { old, new ->
|
||||
checkNotNull(old) { "Fetched message no longer exist!" }
|
||||
@ -152,22 +163,36 @@ class MessageRepository @Inject constructor(
|
||||
subject: String,
|
||||
content: String,
|
||||
recipients: List<Recipient>,
|
||||
mailboxId: String,
|
||||
mailbox: Mailbox,
|
||||
) {
|
||||
sdk.init(student).sendMessage(
|
||||
subject = subject,
|
||||
content = content,
|
||||
recipients = recipients.mapFromEntities(),
|
||||
mailboxId = mailboxId,
|
||||
)
|
||||
wulkanowySdkFactory.create(student)
|
||||
.sendMessage(
|
||||
subject = subject,
|
||||
content = content,
|
||||
recipients = recipients.mapFromEntities(),
|
||||
mailboxId = mailbox.globalKey,
|
||||
)
|
||||
refreshFolders(student, mailbox, listOf(SENT))
|
||||
}
|
||||
|
||||
suspend fun deleteMessages(student: Student, mailbox: Mailbox?, messages: List<Message>) {
|
||||
suspend fun restoreMessages(student: Student, mailbox: Mailbox?, messages: List<Message>) {
|
||||
wulkanowySdkFactory.create(student)
|
||||
.restoreMessages(messages = messages.map { it.messageGlobalKey })
|
||||
|
||||
refreshFolders(student, mailbox)
|
||||
}
|
||||
|
||||
suspend fun deleteMessage(student: Student, message: Message) {
|
||||
deleteMessages(student, listOf(message))
|
||||
}
|
||||
|
||||
suspend fun deleteMessages(student: Student, messages: List<Message>) {
|
||||
val firstMessage = messages.first()
|
||||
sdk.init(student).deleteMessages(
|
||||
messages = messages.map { it.messageGlobalKey },
|
||||
removeForever = firstMessage.folderId == TRASHED.id,
|
||||
)
|
||||
wulkanowySdkFactory.create(student)
|
||||
.deleteMessages(
|
||||
messages = messages.map { it.messageGlobalKey },
|
||||
removeForever = firstMessage.folderId == TRASHED.id,
|
||||
)
|
||||
|
||||
if (firstMessage.folderId != TRASHED.id) {
|
||||
val deletedMessages = messages.map {
|
||||
@ -181,18 +206,24 @@ class MessageRepository @Inject constructor(
|
||||
}
|
||||
|
||||
messagesDb.updateAll(deletedMessages)
|
||||
} else messagesDb.deleteAll(messages)
|
||||
|
||||
getMessages(
|
||||
student = student,
|
||||
mailbox = mailbox,
|
||||
folder = TRASHED,
|
||||
forceRefresh = true,
|
||||
).first()
|
||||
} else {
|
||||
messagesDb.deleteAll(messages)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun deleteMessage(student: Student, mailbox: Mailbox?, message: Message) {
|
||||
deleteMessages(student, mailbox, listOf(message))
|
||||
private suspend fun refreshFolders(
|
||||
student: Student,
|
||||
mailbox: Mailbox?,
|
||||
folders: List<MessageFolder> = MessageFolder.entries
|
||||
) {
|
||||
folders.forEach {
|
||||
getMessages(
|
||||
student = student,
|
||||
mailbox = mailbox,
|
||||
folder = it,
|
||||
forceRefresh = true,
|
||||
).toFirstResult()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getMailboxes(student: Student, forceRefresh: Boolean) = networkBoundResource(
|
||||
@ -206,7 +237,9 @@ class MessageRepository @Inject constructor(
|
||||
},
|
||||
query = { mailboxDao.loadAll(student.email, student.symbol, student.schoolSymbol) },
|
||||
fetch = {
|
||||
sdk.init(student).getMailboxes().mapToEntities(student)
|
||||
wulkanowySdkFactory.create(student)
|
||||
.getMailboxes()
|
||||
.mapToEntities(student)
|
||||
},
|
||||
saveFetchResult = { old, new ->
|
||||
mailboxDao.deleteAll(old uniqueSubtract new)
|
||||
@ -236,4 +269,18 @@ class MessageRepository @Inject constructor(
|
||||
context.getString(R.string.pref_key_message_draft),
|
||||
value?.let { json.encodeToString(it) }
|
||||
)
|
||||
|
||||
private suspend fun isMuted(author: String): Boolean {
|
||||
return mutedMessageSendersDao.checkMute(author)
|
||||
}
|
||||
|
||||
suspend fun muteMessage(author: String) {
|
||||
if (isMuted(author)) return
|
||||
mutedMessageSendersDao.insertMute(MutedMessageSender(author))
|
||||
}
|
||||
|
||||
suspend fun unmuteMessage(author: String) {
|
||||
if (!isMuted(author)) return
|
||||
mutedMessageSendersDao.deleteMute(author)
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
package io.github.wulkanowy.data.repositories
|
||||
|
||||
import io.github.wulkanowy.data.WulkanowySdkFactory
|
||||
import io.github.wulkanowy.data.db.dao.MobileDeviceDao
|
||||
import io.github.wulkanowy.data.db.entities.MobileDevice
|
||||
import io.github.wulkanowy.data.db.entities.Semester
|
||||
@ -8,11 +9,8 @@ import io.github.wulkanowy.data.mappers.mapToEntities
|
||||
import io.github.wulkanowy.data.mappers.mapToMobileDeviceToken
|
||||
import io.github.wulkanowy.data.networkBoundResource
|
||||
import io.github.wulkanowy.data.pojos.MobileDeviceToken
|
||||
import io.github.wulkanowy.sdk.Sdk
|
||||
import io.github.wulkanowy.utils.AutoRefreshHelper
|
||||
import io.github.wulkanowy.utils.getRefreshKey
|
||||
import io.github.wulkanowy.utils.init
|
||||
import io.github.wulkanowy.utils.switchSemester
|
||||
import io.github.wulkanowy.utils.uniqueSubtract
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import javax.inject.Inject
|
||||
@ -21,7 +19,7 @@ import javax.inject.Singleton
|
||||
@Singleton
|
||||
class MobileDeviceRepository @Inject constructor(
|
||||
private val mobileDb: MobileDeviceDao,
|
||||
private val sdk: Sdk,
|
||||
private val wulkanowySdkFactory: WulkanowySdkFactory,
|
||||
private val refreshHelper: AutoRefreshHelper,
|
||||
) {
|
||||
|
||||
@ -42,30 +40,28 @@ class MobileDeviceRepository @Inject constructor(
|
||||
},
|
||||
query = { mobileDb.loadAll(student.userLoginId) },
|
||||
fetch = {
|
||||
sdk.init(student)
|
||||
.switchSemester(semester)
|
||||
wulkanowySdkFactory.create(student, semester)
|
||||
.getRegisteredDevices()
|
||||
.mapToEntities(student)
|
||||
},
|
||||
saveFetchResult = { old, new ->
|
||||
mobileDb.deleteAll(old uniqueSubtract new)
|
||||
mobileDb.insertAll(new uniqueSubtract old)
|
||||
|
||||
mobileDb.removeOldAndSaveNew(
|
||||
oldItems = old uniqueSubtract new,
|
||||
newItems = new uniqueSubtract old,
|
||||
)
|
||||
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, student))
|
||||
}
|
||||
)
|
||||
|
||||
suspend fun unregisterDevice(student: Student, semester: Semester, device: MobileDevice) {
|
||||
sdk.init(student)
|
||||
.switchSemester(semester)
|
||||
wulkanowySdkFactory.create(student, semester)
|
||||
.unregisterDevice(device.deviceId)
|
||||
|
||||
mobileDb.deleteAll(listOf(device))
|
||||
}
|
||||
|
||||
suspend fun getToken(student: Student, semester: Semester): MobileDeviceToken {
|
||||
return sdk.init(student)
|
||||
.switchSemester(semester)
|
||||
return wulkanowySdkFactory.create(student, semester)
|
||||
.getToken()
|
||||
.mapToMobileDeviceToken()
|
||||
}
|
||||
|
@ -1,13 +1,16 @@
|
||||
package io.github.wulkanowy.data.repositories
|
||||
|
||||
import io.github.wulkanowy.data.WulkanowySdkFactory
|
||||
import io.github.wulkanowy.data.db.dao.NoteDao
|
||||
import io.github.wulkanowy.data.db.entities.Note
|
||||
import io.github.wulkanowy.data.db.entities.Semester
|
||||
import io.github.wulkanowy.data.db.entities.Student
|
||||
import io.github.wulkanowy.data.mappers.mapToEntities
|
||||
import io.github.wulkanowy.data.networkBoundResource
|
||||
import io.github.wulkanowy.sdk.Sdk
|
||||
import io.github.wulkanowy.utils.*
|
||||
import io.github.wulkanowy.utils.AutoRefreshHelper
|
||||
import io.github.wulkanowy.utils.getRefreshKey
|
||||
import io.github.wulkanowy.utils.toLocalDate
|
||||
import io.github.wulkanowy.utils.uniqueSubtract
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import javax.inject.Inject
|
||||
@ -16,7 +19,7 @@ import javax.inject.Singleton
|
||||
@Singleton
|
||||
class NoteRepository @Inject constructor(
|
||||
private val noteDb: NoteDao,
|
||||
private val sdk: Sdk,
|
||||
private val wulkanowySdkFactory: WulkanowySdkFactory,
|
||||
private val refreshHelper: AutoRefreshHelper,
|
||||
) {
|
||||
|
||||
@ -40,20 +43,21 @@ class NoteRepository @Inject constructor(
|
||||
},
|
||||
query = { noteDb.loadAll(student.studentId) },
|
||||
fetch = {
|
||||
sdk.init(student)
|
||||
.switchSemester(semester)
|
||||
wulkanowySdkFactory.create(student, semester)
|
||||
.getNotes()
|
||||
.mapToEntities(semester)
|
||||
},
|
||||
saveFetchResult = { old, new ->
|
||||
noteDb.deleteAll(old uniqueSubtract new)
|
||||
noteDb.insertAll((new uniqueSubtract old).onEach {
|
||||
val notesToAdd = (new uniqueSubtract old).onEach {
|
||||
if (it.date >= student.registrationDate.toLocalDate()) it.apply {
|
||||
isRead = false
|
||||
if (notify) isNotified = false
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
noteDb.removeOldAndSaveNew(
|
||||
oldItems = old uniqueSubtract new,
|
||||
newItems = notesToAdd,
|
||||
)
|
||||
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester))
|
||||
}
|
||||
)
|
||||
|
@ -10,6 +10,7 @@ import com.fredporciuncula.flow.preferences.Serializer
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import io.github.wulkanowy.R
|
||||
import io.github.wulkanowy.data.enums.AppTheme
|
||||
import io.github.wulkanowy.data.enums.AttendanceCalculatorSortingMode
|
||||
import io.github.wulkanowy.data.enums.GradeColorTheme
|
||||
import io.github.wulkanowy.data.enums.GradeExpandMode
|
||||
import io.github.wulkanowy.data.enums.GradeSortingMode
|
||||
@ -41,6 +42,27 @@ class PreferencesRepository @Inject constructor(
|
||||
R.bool.pref_default_attendance_present
|
||||
)
|
||||
|
||||
val targetAttendanceFlow: Flow<Int>
|
||||
get() = flowSharedPref.getInt(
|
||||
context.getString(R.string.pref_key_attendance_target),
|
||||
context.resources.getInteger(R.integer.pref_default_attendance_target)
|
||||
).asFlow()
|
||||
|
||||
val attendanceCalculatorSortingModeFlow: Flow<AttendanceCalculatorSortingMode>
|
||||
get() = flowSharedPref.getString(
|
||||
context.getString(R.string.pref_key_attendance_calculator_sorting_mode),
|
||||
context.resources.getString(R.string.pref_default_attendance_calculator_sorting_mode)
|
||||
).asFlow().map(AttendanceCalculatorSortingMode::getByValue)
|
||||
|
||||
/**
|
||||
* Subjects are empty when they don't have any attendances (total = 0, attendances = 0, absences = 0).
|
||||
*/
|
||||
val attendanceCalculatorShowEmptySubjects: Flow<Boolean>
|
||||
get() = flowSharedPref.getBoolean(
|
||||
context.getString(R.string.pref_key_attendance_calculator_show_empty_subjects),
|
||||
context.resources.getBoolean(R.bool.pref_default_attendance_calculator_show_empty_subjects)
|
||||
).asFlow()
|
||||
|
||||
private val gradeAverageModePref: Preference<GradeAverageMode>
|
||||
get() = getObjectFlow(
|
||||
R.string.pref_key_grade_average_mode,
|
||||
|
@ -1,12 +1,15 @@
|
||||
package io.github.wulkanowy.data.repositories
|
||||
|
||||
import io.github.wulkanowy.data.WulkanowySdkFactory
|
||||
import io.github.wulkanowy.data.db.dao.RecipientDao
|
||||
import io.github.wulkanowy.data.db.entities.*
|
||||
import io.github.wulkanowy.data.db.entities.Mailbox
|
||||
import io.github.wulkanowy.data.db.entities.MailboxType
|
||||
import io.github.wulkanowy.data.db.entities.Message
|
||||
import io.github.wulkanowy.data.db.entities.Recipient
|
||||
import io.github.wulkanowy.data.db.entities.Student
|
||||
import io.github.wulkanowy.data.mappers.mapToEntities
|
||||
import io.github.wulkanowy.sdk.Sdk
|
||||
import io.github.wulkanowy.utils.AutoRefreshHelper
|
||||
import io.github.wulkanowy.utils.getRefreshKey
|
||||
import io.github.wulkanowy.utils.init
|
||||
import io.github.wulkanowy.utils.uniqueSubtract
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
@ -14,19 +17,22 @@ import javax.inject.Singleton
|
||||
@Singleton
|
||||
class RecipientRepository @Inject constructor(
|
||||
private val recipientDb: RecipientDao,
|
||||
private val sdk: Sdk,
|
||||
private val wulkanowySdkFactory: WulkanowySdkFactory,
|
||||
private val refreshHelper: AutoRefreshHelper,
|
||||
) {
|
||||
|
||||
private val cacheKey = "recipient"
|
||||
|
||||
suspend fun refreshRecipients(student: Student, mailbox: Mailbox, type: MailboxType) {
|
||||
val new = sdk.init(student).getRecipients(mailbox.globalKey)
|
||||
val new = wulkanowySdkFactory.create(student)
|
||||
.getRecipients(mailbox.globalKey)
|
||||
.mapToEntities(mailbox.globalKey)
|
||||
val old = recipientDb.loadAll(type, mailbox.globalKey)
|
||||
|
||||
recipientDb.deleteAll(old uniqueSubtract new)
|
||||
recipientDb.insertAll(new uniqueSubtract old)
|
||||
recipientDb.removeOldAndSaveNew(
|
||||
oldItems = old uniqueSubtract new,
|
||||
newItems = new uniqueSubtract old,
|
||||
)
|
||||
|
||||
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, student))
|
||||
}
|
||||
@ -54,7 +60,7 @@ class RecipientRepository @Inject constructor(
|
||||
): List<Recipient> {
|
||||
mailbox ?: return emptyList()
|
||||
|
||||
return sdk.init(student)
|
||||
return wulkanowySdkFactory.create(student)
|
||||
.getMessageReplayDetails(message.messageGlobalKey)
|
||||
.sender
|
||||
.let(::listOf)
|
||||
|
@ -1,17 +1,23 @@
|
||||
package io.github.wulkanowy.data.repositories
|
||||
|
||||
import io.github.wulkanowy.sdk.Sdk
|
||||
import io.github.wulkanowy.data.WulkanowySdkFactory
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class RecoverRepository @Inject constructor(private val sdk: Sdk) {
|
||||
class RecoverRepository @Inject constructor(
|
||||
private val wulkanowySdkFactory: WulkanowySdkFactory
|
||||
) {
|
||||
|
||||
suspend fun getReCaptchaSiteKey(host: String, symbol: String): Pair<String, String> {
|
||||
return sdk.getPasswordResetCaptchaCode(host, symbol)
|
||||
}
|
||||
suspend fun getReCaptchaSiteKey(host: String, symbol: String): Pair<String, String> =
|
||||
wulkanowySdkFactory.create()
|
||||
.getPasswordResetCaptchaCode(host, symbol)
|
||||
|
||||
suspend fun sendRecoverRequest(
|
||||
url: String, symbol: String, email: String, reCaptchaResponse: String
|
||||
): String = sdk.sendPasswordResetRequest(url, symbol, email, reCaptchaResponse)
|
||||
url: String,
|
||||
symbol: String,
|
||||
email: String,
|
||||
reCaptchaResponse: String
|
||||
): String = wulkanowySdkFactory.create()
|
||||
.sendPasswordResetRequest(url, symbol, email, reCaptchaResponse)
|
||||
}
|
||||
|
@ -1,14 +1,13 @@
|
||||
package io.github.wulkanowy.data.repositories
|
||||
|
||||
import io.github.wulkanowy.data.WulkanowySdkFactory
|
||||
import io.github.wulkanowy.data.db.dao.SchoolAnnouncementDao
|
||||
import io.github.wulkanowy.data.db.entities.SchoolAnnouncement
|
||||
import io.github.wulkanowy.data.db.entities.Student
|
||||
import io.github.wulkanowy.data.mappers.mapToEntities
|
||||
import io.github.wulkanowy.data.networkBoundResource
|
||||
import io.github.wulkanowy.sdk.Sdk
|
||||
import io.github.wulkanowy.utils.AutoRefreshHelper
|
||||
import io.github.wulkanowy.utils.getRefreshKey
|
||||
import io.github.wulkanowy.utils.init
|
||||
import io.github.wulkanowy.utils.uniqueSubtract
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
@ -18,7 +17,7 @@ import javax.inject.Singleton
|
||||
@Singleton
|
||||
class SchoolAnnouncementRepository @Inject constructor(
|
||||
private val schoolAnnouncementDb: SchoolAnnouncementDao,
|
||||
private val sdk: Sdk,
|
||||
private val wulkanowySdkFactory: WulkanowySdkFactory,
|
||||
private val refreshHelper: AutoRefreshHelper,
|
||||
) {
|
||||
|
||||
@ -41,17 +40,18 @@ class SchoolAnnouncementRepository @Inject constructor(
|
||||
schoolAnnouncementDb.loadAll(student.userLoginId)
|
||||
},
|
||||
fetch = {
|
||||
sdk.init(student)
|
||||
.getDirectorInformation()
|
||||
.mapToEntities(student)
|
||||
val sdk = wulkanowySdkFactory.create(student)
|
||||
val lastAnnouncements = sdk.getLastAnnouncements().mapToEntities(student)
|
||||
val directorInformation = sdk.getDirectorInformation().mapToEntities(student)
|
||||
lastAnnouncements + directorInformation
|
||||
},
|
||||
saveFetchResult = { old, new ->
|
||||
val schoolAnnouncementsToSave = (new uniqueSubtract old).onEach {
|
||||
if (notify) it.isNotified = false
|
||||
}
|
||||
|
||||
schoolAnnouncementDb.deleteAll(old uniqueSubtract new)
|
||||
schoolAnnouncementDb.insertAll(schoolAnnouncementsToSave)
|
||||
schoolAnnouncementDb.removeOldAndSaveNew(
|
||||
oldItems = old uniqueSubtract new,
|
||||
newItems = (new uniqueSubtract old).onEach {
|
||||
if (notify) it.isNotified = false
|
||||
},
|
||||
)
|
||||
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, student))
|
||||
}
|
||||
)
|
||||
|
@ -1,15 +1,13 @@
|
||||
package io.github.wulkanowy.data.repositories
|
||||
|
||||
import io.github.wulkanowy.data.WulkanowySdkFactory
|
||||
import io.github.wulkanowy.data.db.dao.SchoolDao
|
||||
import io.github.wulkanowy.data.db.entities.Semester
|
||||
import io.github.wulkanowy.data.db.entities.Student
|
||||
import io.github.wulkanowy.data.mappers.mapToEntity
|
||||
import io.github.wulkanowy.data.networkBoundResource
|
||||
import io.github.wulkanowy.sdk.Sdk
|
||||
import io.github.wulkanowy.utils.AutoRefreshHelper
|
||||
import io.github.wulkanowy.utils.getRefreshKey
|
||||
import io.github.wulkanowy.utils.init
|
||||
import io.github.wulkanowy.utils.switchSemester
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
@ -17,7 +15,7 @@ import javax.inject.Singleton
|
||||
@Singleton
|
||||
class SchoolRepository @Inject constructor(
|
||||
private val schoolDb: SchoolDao,
|
||||
private val sdk: Sdk,
|
||||
private val wulkanowySdkFactory: WulkanowySdkFactory,
|
||||
private val refreshHelper: AutoRefreshHelper,
|
||||
) {
|
||||
|
||||
@ -40,17 +38,16 @@ class SchoolRepository @Inject constructor(
|
||||
},
|
||||
query = { schoolDb.load(semester.studentId, semester.classId) },
|
||||
fetch = {
|
||||
sdk.init(student)
|
||||
.switchSemester(semester)
|
||||
wulkanowySdkFactory.create(student, semester)
|
||||
.getSchool()
|
||||
.mapToEntity(semester)
|
||||
},
|
||||
saveFetchResult = { old, new ->
|
||||
if (old != null && new != old) {
|
||||
with(schoolDb) {
|
||||
deleteAll(listOf(old))
|
||||
insertAll(listOf(new))
|
||||
}
|
||||
schoolDb.removeOldAndSaveNew(
|
||||
oldItems = listOf(old),
|
||||
newItems = listOf(new)
|
||||
)
|
||||
} else if (old == null) {
|
||||
schoolDb.insertAll(listOf(new))
|
||||
}
|
||||
|
@ -1,17 +1,15 @@
|
||||
package io.github.wulkanowy.data.repositories
|
||||
|
||||
import io.github.wulkanowy.data.WulkanowySdkFactory
|
||||
import io.github.wulkanowy.data.api.SchoolsService
|
||||
import io.github.wulkanowy.data.db.entities.Semester
|
||||
import io.github.wulkanowy.data.db.entities.Student
|
||||
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
|
||||
import io.github.wulkanowy.data.pojos.IntegrityRequest
|
||||
import io.github.wulkanowy.data.pojos.LoginEvent
|
||||
import io.github.wulkanowy.sdk.Sdk
|
||||
import io.github.wulkanowy.ui.modules.login.LoginData
|
||||
import io.github.wulkanowy.utils.IntegrityHelper
|
||||
import io.github.wulkanowy.utils.getCurrentOrLast
|
||||
import io.github.wulkanowy.utils.init
|
||||
import io.github.wulkanowy.utils.switchSemester
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import timber.log.Timber
|
||||
import java.util.UUID
|
||||
@ -23,7 +21,7 @@ import kotlin.time.Duration.Companion.seconds
|
||||
class SchoolsRepository @Inject constructor(
|
||||
private val integrityHelper: IntegrityHelper,
|
||||
private val schoolsService: SchoolsService,
|
||||
private val sdk: Sdk,
|
||||
private val wulkanowySdkFactory: WulkanowySdkFactory,
|
||||
) {
|
||||
|
||||
suspend fun logSchoolLogin(loginData: LoginData, students: List<StudentWithSemesters>) {
|
||||
@ -40,10 +38,9 @@ class SchoolsRepository @Inject constructor(
|
||||
private suspend fun logLogin(loginData: LoginData, student: Student, semester: Semester) {
|
||||
val requestId = UUID.randomUUID().toString()
|
||||
val token = integrityHelper.getIntegrityToken(requestId) ?: return
|
||||
val updatedStudent = student.copy(password = loginData.password)
|
||||
|
||||
val schoolInfo = sdk
|
||||
.init(student.copy(password = loginData.password))
|
||||
.switchSemester(semester)
|
||||
val schoolInfo = wulkanowySdkFactory.create(updatedStudent, semester)
|
||||
.getSchool()
|
||||
|
||||
schoolsService.logLoginEvent(
|
||||
|
@ -1,11 +1,15 @@
|
||||
package io.github.wulkanowy.data.repositories
|
||||
|
||||
import io.github.wulkanowy.data.WulkanowySdkFactory
|
||||
import io.github.wulkanowy.data.db.dao.SemesterDao
|
||||
import io.github.wulkanowy.data.db.entities.Semester
|
||||
import io.github.wulkanowy.data.db.entities.Student
|
||||
import io.github.wulkanowy.data.mappers.mapToEntities
|
||||
import io.github.wulkanowy.sdk.Sdk
|
||||
import io.github.wulkanowy.utils.*
|
||||
import io.github.wulkanowy.utils.DispatchersProvider
|
||||
import io.github.wulkanowy.utils.getCurrentOrLast
|
||||
import io.github.wulkanowy.utils.isCurrent
|
||||
import io.github.wulkanowy.utils.uniqueSubtract
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
@ -14,8 +18,8 @@ import javax.inject.Singleton
|
||||
@Singleton
|
||||
class SemesterRepository @Inject constructor(
|
||||
private val semesterDb: SemesterDao,
|
||||
private val sdk: Sdk,
|
||||
private val dispatchers: DispatchersProvider
|
||||
private val wulkanowySdkFactory: WulkanowySdkFactory,
|
||||
private val dispatchers: DispatchersProvider,
|
||||
) {
|
||||
|
||||
suspend fun getSemesters(
|
||||
@ -45,6 +49,7 @@ class SemesterRepository @Inject constructor(
|
||||
0 == it.diaryId && 0 == it.kindergartenDiaryId
|
||||
} == true
|
||||
}
|
||||
|
||||
else -> false
|
||||
}
|
||||
|
||||
@ -55,12 +60,17 @@ class SemesterRepository @Inject constructor(
|
||||
}
|
||||
|
||||
private suspend fun refreshSemesters(student: Student) {
|
||||
val new = sdk.init(student).getSemesters().mapToEntities(student.studentId)
|
||||
val new = wulkanowySdkFactory.create(student)
|
||||
.getSemesters()
|
||||
.mapToEntities(student.studentId)
|
||||
|
||||
if (new.isEmpty()) return Timber.i("Empty semester list!")
|
||||
|
||||
val old = semesterDb.loadAll(student.studentId, student.classId)
|
||||
semesterDb.deleteAll(old.uniqueSubtract(new))
|
||||
semesterDb.insertSemesters(new.uniqueSubtract(old))
|
||||
semesterDb.removeOldAndSaveNew(
|
||||
oldItems = old uniqueSubtract new,
|
||||
newItems = new uniqueSubtract old,
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun getCurrentSemester(student: Student, forceRefresh: Boolean = false) =
|
||||
|
@ -1,13 +1,11 @@
|
||||
package io.github.wulkanowy.data.repositories
|
||||
|
||||
import io.github.wulkanowy.data.WulkanowySdkFactory
|
||||
import io.github.wulkanowy.data.db.dao.StudentInfoDao
|
||||
import io.github.wulkanowy.data.db.entities.Semester
|
||||
import io.github.wulkanowy.data.db.entities.Student
|
||||
import io.github.wulkanowy.data.mappers.mapToEntity
|
||||
import io.github.wulkanowy.data.networkBoundResource
|
||||
import io.github.wulkanowy.sdk.Sdk
|
||||
import io.github.wulkanowy.utils.init
|
||||
import io.github.wulkanowy.utils.switchSemester
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
@ -15,7 +13,7 @@ import javax.inject.Singleton
|
||||
@Singleton
|
||||
class StudentInfoRepository @Inject constructor(
|
||||
private val studentInfoDao: StudentInfoDao,
|
||||
private val sdk: Sdk
|
||||
private val wulkanowySdkFactory: WulkanowySdkFactory,
|
||||
) {
|
||||
|
||||
private val saveFetchResultMutex = Mutex()
|
||||
@ -30,16 +28,16 @@ class StudentInfoRepository @Inject constructor(
|
||||
shouldFetch = { it == null || forceRefresh },
|
||||
query = { studentInfoDao.loadStudentInfo(student.studentId) },
|
||||
fetch = {
|
||||
sdk.init(student)
|
||||
.switchSemester(semester)
|
||||
.getStudentInfo().mapToEntity(semester)
|
||||
wulkanowySdkFactory.create(student, semester)
|
||||
.getStudentInfo()
|
||||
.mapToEntity(semester)
|
||||
},
|
||||
saveFetchResult = { old, new ->
|
||||
if (old != null && new != old) {
|
||||
with(studentInfoDao) {
|
||||
deleteAll(listOf(old))
|
||||
insertAll(listOf(new))
|
||||
}
|
||||
studentInfoDao.removeOldAndSaveNew(
|
||||
oldItems = listOf(old),
|
||||
newItems = listOf(new),
|
||||
)
|
||||
} else if (old == null) {
|
||||
studentInfoDao.insertAll(listOf(new))
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package io.github.wulkanowy.data.repositories
|
||||
|
||||
import androidx.room.withTransaction
|
||||
import io.github.wulkanowy.data.WulkanowySdkFactory
|
||||
import io.github.wulkanowy.data.db.AppDatabase
|
||||
import io.github.wulkanowy.data.db.dao.SemesterDao
|
||||
import io.github.wulkanowy.data.db.dao.StudentDao
|
||||
@ -14,9 +15,7 @@ import io.github.wulkanowy.data.mappers.mapToPojo
|
||||
import io.github.wulkanowy.data.pojos.RegisterUser
|
||||
import io.github.wulkanowy.sdk.Sdk
|
||||
import io.github.wulkanowy.utils.DispatchersProvider
|
||||
import io.github.wulkanowy.utils.init
|
||||
import io.github.wulkanowy.utils.security.Scrambler
|
||||
import io.github.wulkanowy.utils.switchSemester
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
@ -26,7 +25,7 @@ class StudentRepository @Inject constructor(
|
||||
private val dispatchers: DispatchersProvider,
|
||||
private val studentDb: StudentDao,
|
||||
private val semesterDb: SemesterDao,
|
||||
private val sdk: Sdk,
|
||||
private val wulkanowySdkFactory: WulkanowySdkFactory,
|
||||
private val appDatabase: AppDatabase,
|
||||
private val scrambler: Scrambler,
|
||||
) {
|
||||
@ -37,7 +36,7 @@ class StudentRepository @Inject constructor(
|
||||
pin: String,
|
||||
symbol: String,
|
||||
token: String
|
||||
): RegisterUser = sdk
|
||||
): RegisterUser = wulkanowySdkFactory.create()
|
||||
.getStudentsFromHebe(token, pin, symbol, "")
|
||||
.mapToPojo(null)
|
||||
|
||||
@ -47,7 +46,7 @@ class StudentRepository @Inject constructor(
|
||||
scrapperBaseUrl: String,
|
||||
domainSuffix: String,
|
||||
symbol: String
|
||||
): RegisterUser = sdk
|
||||
): RegisterUser = wulkanowySdkFactory.create()
|
||||
.getUserSubjectsFromScrapper(email, password, scrapperBaseUrl, domainSuffix, symbol)
|
||||
.mapToPojo(password)
|
||||
|
||||
@ -56,7 +55,7 @@ class StudentRepository @Inject constructor(
|
||||
password: String,
|
||||
scrapperBaseUrl: String,
|
||||
symbol: String
|
||||
): RegisterUser = sdk
|
||||
): RegisterUser = wulkanowySdkFactory.create()
|
||||
.getStudentsHybrid(email, password, scrapperBaseUrl, "", symbol)
|
||||
.mapToPojo(password)
|
||||
|
||||
@ -149,13 +148,11 @@ class StudentRepository @Inject constructor(
|
||||
.distinctBy { it.student.studentName }.size == 1
|
||||
|
||||
suspend fun authorizePermission(student: Student, semester: Semester, pesel: String) =
|
||||
sdk.init(student)
|
||||
.switchSemester(semester)
|
||||
wulkanowySdkFactory.create(student, semester)
|
||||
.authorizePermission(pesel)
|
||||
|
||||
suspend fun refreshStudentName(student: Student, semester: Semester) {
|
||||
val newCurrentApiStudent = sdk.init(student)
|
||||
.switchSemester(semester)
|
||||
val newCurrentApiStudent = wulkanowySdkFactory.create(student, semester)
|
||||
.getCurrentStudent() ?: return
|
||||
|
||||
val studentName = StudentName(
|
||||
|
@ -1,15 +1,13 @@
|
||||
package io.github.wulkanowy.data.repositories
|
||||
|
||||
import io.github.wulkanowy.data.WulkanowySdkFactory
|
||||
import io.github.wulkanowy.data.db.dao.SubjectDao
|
||||
import io.github.wulkanowy.data.db.entities.Semester
|
||||
import io.github.wulkanowy.data.db.entities.Student
|
||||
import io.github.wulkanowy.data.mappers.mapToEntities
|
||||
import io.github.wulkanowy.data.networkBoundResource
|
||||
import io.github.wulkanowy.sdk.Sdk
|
||||
import io.github.wulkanowy.utils.AutoRefreshHelper
|
||||
import io.github.wulkanowy.utils.getRefreshKey
|
||||
import io.github.wulkanowy.utils.init
|
||||
import io.github.wulkanowy.utils.switchSemester
|
||||
import io.github.wulkanowy.utils.uniqueSubtract
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import javax.inject.Inject
|
||||
@ -18,7 +16,7 @@ import javax.inject.Singleton
|
||||
@Singleton
|
||||
class SubjectRepository @Inject constructor(
|
||||
private val subjectDao: SubjectDao,
|
||||
private val sdk: Sdk,
|
||||
private val wulkanowySdkFactory: WulkanowySdkFactory,
|
||||
private val refreshHelper: AutoRefreshHelper,
|
||||
) {
|
||||
|
||||
@ -39,15 +37,15 @@ class SubjectRepository @Inject constructor(
|
||||
},
|
||||
query = { subjectDao.loadAll(semester.diaryId, semester.studentId) },
|
||||
fetch = {
|
||||
sdk.init(student)
|
||||
.switchSemester(semester)
|
||||
wulkanowySdkFactory.create(student, semester)
|
||||
.getSubjects()
|
||||
.mapToEntities(semester)
|
||||
},
|
||||
saveFetchResult = { old, new ->
|
||||
subjectDao.deleteAll(old uniqueSubtract new)
|
||||
subjectDao.insertAll(new uniqueSubtract old)
|
||||
|
||||
subjectDao.removeOldAndSaveNew(
|
||||
oldItems = old uniqueSubtract new,
|
||||
newItems = new uniqueSubtract old
|
||||
)
|
||||
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester))
|
||||
}
|
||||
)
|
||||
|
@ -1,15 +1,13 @@
|
||||
package io.github.wulkanowy.data.repositories
|
||||
|
||||
import io.github.wulkanowy.data.WulkanowySdkFactory
|
||||
import io.github.wulkanowy.data.db.dao.TeacherDao
|
||||
import io.github.wulkanowy.data.db.entities.Semester
|
||||
import io.github.wulkanowy.data.db.entities.Student
|
||||
import io.github.wulkanowy.data.mappers.mapToEntities
|
||||
import io.github.wulkanowy.data.networkBoundResource
|
||||
import io.github.wulkanowy.sdk.Sdk
|
||||
import io.github.wulkanowy.utils.AutoRefreshHelper
|
||||
import io.github.wulkanowy.utils.getRefreshKey
|
||||
import io.github.wulkanowy.utils.init
|
||||
import io.github.wulkanowy.utils.switchSemester
|
||||
import io.github.wulkanowy.utils.uniqueSubtract
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import javax.inject.Inject
|
||||
@ -18,7 +16,7 @@ import javax.inject.Singleton
|
||||
@Singleton
|
||||
class TeacherRepository @Inject constructor(
|
||||
private val teacherDb: TeacherDao,
|
||||
private val sdk: Sdk,
|
||||
private val wulkanowySdkFactory: WulkanowySdkFactory,
|
||||
private val refreshHelper: AutoRefreshHelper,
|
||||
) {
|
||||
|
||||
@ -39,15 +37,15 @@ class TeacherRepository @Inject constructor(
|
||||
},
|
||||
query = { teacherDb.loadAll(semester.studentId, semester.classId) },
|
||||
fetch = {
|
||||
sdk.init(student)
|
||||
.switchSemester(semester)
|
||||
wulkanowySdkFactory.create(student, semester)
|
||||
.getTeachers()
|
||||
.mapToEntities(semester)
|
||||
},
|
||||
saveFetchResult = { old, new ->
|
||||
teacherDb.deleteAll(old uniqueSubtract new)
|
||||
teacherDb.insertAll(new uniqueSubtract old)
|
||||
|
||||
teacherDb.removeOldAndSaveNew(
|
||||
oldItems = old uniqueSubtract new,
|
||||
newItems = new uniqueSubtract old,
|
||||
)
|
||||
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester))
|
||||
}
|
||||
)
|
||||
|
@ -1,15 +1,23 @@
|
||||
package io.github.wulkanowy.data.repositories
|
||||
|
||||
import io.github.wulkanowy.data.WulkanowySdkFactory
|
||||
import io.github.wulkanowy.data.db.dao.TimetableAdditionalDao
|
||||
import io.github.wulkanowy.data.db.dao.TimetableDao
|
||||
import io.github.wulkanowy.data.db.dao.TimetableHeaderDao
|
||||
import io.github.wulkanowy.data.db.entities.*
|
||||
import io.github.wulkanowy.data.db.entities.Semester
|
||||
import io.github.wulkanowy.data.db.entities.Student
|
||||
import io.github.wulkanowy.data.db.entities.Timetable
|
||||
import io.github.wulkanowy.data.db.entities.TimetableAdditional
|
||||
import io.github.wulkanowy.data.db.entities.TimetableHeader
|
||||
import io.github.wulkanowy.data.mappers.mapToEntities
|
||||
import io.github.wulkanowy.data.networkBoundResource
|
||||
import io.github.wulkanowy.data.pojos.TimetableFull
|
||||
import io.github.wulkanowy.sdk.Sdk
|
||||
import io.github.wulkanowy.services.alarm.TimetableNotificationSchedulerHelper
|
||||
import io.github.wulkanowy.utils.*
|
||||
import io.github.wulkanowy.utils.AutoRefreshHelper
|
||||
import io.github.wulkanowy.utils.getRefreshKey
|
||||
import io.github.wulkanowy.utils.monday
|
||||
import io.github.wulkanowy.utils.sunday
|
||||
import io.github.wulkanowy.utils.uniqueSubtract
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
@ -23,7 +31,7 @@ class TimetableRepository @Inject constructor(
|
||||
private val timetableDb: TimetableDao,
|
||||
private val timetableAdditionalDb: TimetableAdditionalDao,
|
||||
private val timetableHeaderDb: TimetableHeaderDao,
|
||||
private val sdk: Sdk,
|
||||
private val wulkanowySdkFactory: WulkanowySdkFactory,
|
||||
private val schedulerHelper: TimetableNotificationSchedulerHelper,
|
||||
private val refreshHelper: AutoRefreshHelper,
|
||||
) {
|
||||
@ -64,8 +72,7 @@ class TimetableRepository @Inject constructor(
|
||||
},
|
||||
query = { getFullTimetableFromDatabase(student, semester, start, end) },
|
||||
fetch = {
|
||||
val timetableFull = sdk.init(student)
|
||||
.switchSemester(semester)
|
||||
val timetableFull = wulkanowySdkFactory.create(student, semester)
|
||||
.getTimetable(start.monday, end.sunday)
|
||||
|
||||
timetableFull.mapToEntities(semester)
|
||||
@ -121,12 +128,12 @@ class TimetableRepository @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun getTimetableFromDatabase(
|
||||
suspend fun getTimetableFromDatabase(
|
||||
semester: Semester,
|
||||
from: LocalDate,
|
||||
start: LocalDate,
|
||||
end: LocalDate
|
||||
): Flow<List<Timetable>> {
|
||||
return timetableDb.loadAll(semester.diaryId, semester.studentId, from, end)
|
||||
): List<Timetable> {
|
||||
return timetableDb.load(semester.diaryId, semester.studentId, start, end)
|
||||
}
|
||||
|
||||
suspend fun updateTimetable(timetable: List<Timetable>) {
|
||||
@ -144,8 +151,10 @@ class TimetableRepository @Inject constructor(
|
||||
new.apply { if (notify) isNotified = false }
|
||||
}
|
||||
|
||||
timetableDb.deleteAll(lessonsToRemove)
|
||||
timetableDb.insertAll(lessonsToAdd)
|
||||
timetableDb.removeOldAndSaveNew(
|
||||
oldItems = lessonsToRemove,
|
||||
newItems = lessonsToAdd,
|
||||
)
|
||||
|
||||
schedulerHelper.cancelScheduled(lessonsToRemove, student)
|
||||
schedulerHelper.scheduleNotifications(lessonsToAdd, student)
|
||||
@ -156,13 +165,17 @@ class TimetableRepository @Inject constructor(
|
||||
new: List<TimetableAdditional>
|
||||
) {
|
||||
val oldFiltered = old.filter { !it.isAddedByUser }
|
||||
timetableAdditionalDb.deleteAll(oldFiltered uniqueSubtract new)
|
||||
timetableAdditionalDb.insertAll(new uniqueSubtract old)
|
||||
timetableAdditionalDb.removeOldAndSaveNew(
|
||||
oldItems = oldFiltered uniqueSubtract new,
|
||||
newItems = new uniqueSubtract old,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun refreshDayHeaders(old: List<TimetableHeader>, new: List<TimetableHeader>) {
|
||||
timetableHeaderDb.deleteAll(old uniqueSubtract new)
|
||||
timetableHeaderDb.insertAll(new uniqueSubtract old)
|
||||
timetableHeaderDb.removeOldAndSaveNew(
|
||||
oldItems = old uniqueSubtract new,
|
||||
newItems = new uniqueSubtract old,
|
||||
)
|
||||
}
|
||||
|
||||
fun getLastRefreshTimestamp(semester: Semester, start: LocalDate, end: LocalDate): Instant {
|
||||
|
@ -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)
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
package io.github.wulkanowy.domain.timetable
|
||||
|
||||
import io.github.wulkanowy.data.db.entities.Semester
|
||||
import io.github.wulkanowy.data.repositories.TimetableRepository
|
||||
import io.github.wulkanowy.utils.monday
|
||||
import io.github.wulkanowy.utils.sunday
|
||||
import java.time.LocalDate
|
||||
import javax.inject.Inject
|
||||
|
||||
class IsStudentHasLessonsOnWeekendUseCase @Inject constructor(
|
||||
private val timetableRepository: TimetableRepository,
|
||||
private val isWeekendHasLessonsUseCase: IsWeekendHasLessonsUseCase,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(
|
||||
semester: Semester,
|
||||
currentDate: LocalDate = LocalDate.now(),
|
||||
): Boolean {
|
||||
val lessons = timetableRepository.getTimetableFromDatabase(
|
||||
semester = semester,
|
||||
start = currentDate.monday,
|
||||
end = currentDate.sunday,
|
||||
)
|
||||
return isWeekendHasLessonsUseCase(lessons)
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package io.github.wulkanowy.domain.timetable
|
||||
|
||||
import io.github.wulkanowy.data.db.entities.Timetable
|
||||
import java.time.DayOfWeek
|
||||
import javax.inject.Inject
|
||||
|
||||
class IsWeekendHasLessonsUseCase @Inject constructor() {
|
||||
|
||||
operator fun invoke(
|
||||
lessons: List<Timetable>,
|
||||
): Boolean = lessons.any {
|
||||
it.date.dayOfWeek in listOf(
|
||||
DayOfWeek.SATURDAY,
|
||||
DayOfWeek.SUNDAY,
|
||||
)
|
||||
}
|
||||
}
|
@ -4,12 +4,12 @@ import android.content.Context
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import io.github.wulkanowy.R
|
||||
import io.github.wulkanowy.data.db.entities.Grade
|
||||
import io.github.wulkanowy.data.db.entities.GradeDescriptive
|
||||
import io.github.wulkanowy.data.db.entities.GradeSummary
|
||||
import io.github.wulkanowy.data.db.entities.Student
|
||||
import io.github.wulkanowy.data.pojos.GroupNotificationData
|
||||
import io.github.wulkanowy.data.pojos.NotificationData
|
||||
import io.github.wulkanowy.ui.modules.Destination
|
||||
import io.github.wulkanowy.ui.modules.splash.SplashActivity
|
||||
import io.github.wulkanowy.utils.getPlural
|
||||
import javax.inject.Inject
|
||||
|
||||
@ -88,4 +88,28 @@ class NewGradeNotification @Inject constructor(
|
||||
|
||||
appNotificationManager.sendMultipleNotifications(groupNotificationData, student)
|
||||
}
|
||||
|
||||
suspend fun notifyDescriptive(items: List<GradeDescriptive>, student: Student) {
|
||||
val notificationDataList = items.map {
|
||||
NotificationData(
|
||||
title = context.getPlural(R.plurals.grade_new_items_descriptive, 1),
|
||||
content = "${it.subject}: ${it.description}",
|
||||
destination = Destination.Grade,
|
||||
)
|
||||
}
|
||||
|
||||
val groupNotificationData = GroupNotificationData(
|
||||
notificationDataList = notificationDataList,
|
||||
title = context.getPlural(R.plurals.grade_new_items_descriptive, items.size),
|
||||
content = context.getPlural(
|
||||
R.plurals.grade_notify_new_items_descriptive,
|
||||
items.size,
|
||||
items.size
|
||||
),
|
||||
destination = Destination.Grade,
|
||||
type = NotificationType.NEW_GRADE_DESCRIPTIVE
|
||||
)
|
||||
|
||||
appNotificationManager.sendMultipleNotifications(groupNotificationData, student)
|
||||
}
|
||||
}
|
||||
|
@ -37,6 +37,10 @@ enum class NotificationType(
|
||||
channel = NewGradesChannel.CHANNEL_ID,
|
||||
icon = R.drawable.ic_stat_grade,
|
||||
),
|
||||
NEW_GRADE_DESCRIPTIVE(
|
||||
channel = NewGradesChannel.CHANNEL_ID,
|
||||
icon = R.drawable.ic_stat_grade,
|
||||
),
|
||||
NEW_HOMEWORK(
|
||||
channel = NewHomeworkChannel.CHANNEL_ID,
|
||||
icon = R.drawable.ic_more_homework,
|
||||
|
@ -45,5 +45,15 @@ class GradeWork @Inject constructor(
|
||||
grade.isFinalGradeNotified = true
|
||||
})
|
||||
}
|
||||
|
||||
gradeRepository.getGradesDescriptiveFromDatabase(semester).first()
|
||||
.filter { !it.isNotified }
|
||||
.let {
|
||||
if (it.isNotEmpty()) newGradeNotification.notifyDescriptive(it, student)
|
||||
|
||||
gradeRepository.updateGradesDescriptive(it.onEach { grade ->
|
||||
grade.isNotified = true
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,6 @@ import io.github.wulkanowy.data.repositories.TimetableRepository
|
||||
import io.github.wulkanowy.data.waitForResult
|
||||
import io.github.wulkanowy.services.sync.notifications.ChangeTimetableNotification
|
||||
import io.github.wulkanowy.utils.nextOrSameSchoolDay
|
||||
import kotlinx.coroutines.flow.first
|
||||
import java.time.LocalDate.now
|
||||
import javax.inject.Inject
|
||||
|
||||
@ -31,10 +30,9 @@ class TimetableWork @Inject constructor(
|
||||
|
||||
timetableRepository.getTimetableFromDatabase(
|
||||
semester = semester,
|
||||
from = startDate,
|
||||
start = startDate,
|
||||
end = endDate,
|
||||
)
|
||||
.first()
|
||||
.filterNot { it.isNotified }
|
||||
.let {
|
||||
if (it.isNotEmpty()) changeTimetableNotification.notify(it, student)
|
||||
|
@ -1,6 +1,7 @@
|
||||
package io.github.wulkanowy.ui.base
|
||||
|
||||
import android.app.ActivityManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
@ -17,6 +18,8 @@ import io.github.wulkanowy.utils.FragmentLifecycleLogger
|
||||
import io.github.wulkanowy.utils.getThemeAttrColor
|
||||
import io.github.wulkanowy.utils.lifecycleAwareVariable
|
||||
import io.github.wulkanowy.utils.openInternetBrowser
|
||||
import timber.log.Timber
|
||||
import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
|
||||
abstract class BaseActivity<T : BasePresenter<out BaseView>, VB : ViewBinding> :
|
||||
@ -36,16 +39,26 @@ abstract class BaseActivity<T : BasePresenter<out BaseView>, VB : ViewBinding> :
|
||||
|
||||
abstract var presenter: T
|
||||
|
||||
private var lastDialogOpenTime = mutableMapOf<String, Instant>()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
inject()
|
||||
themeManager.applyActivityTheme(this)
|
||||
super.onCreate(savedInstanceState)
|
||||
supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleLogger, true)
|
||||
applyCustomTaskDescription()
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
setTaskDescription(
|
||||
ActivityManager.TaskDescription(null, null, getThemeAttrColor(R.attr.colorSurface))
|
||||
)
|
||||
@Suppress("DEPRECATION")
|
||||
private fun applyCustomTaskDescription() {
|
||||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.R) return
|
||||
try {
|
||||
val newColor = getThemeAttrColor(R.attr.colorSurface)
|
||||
val taskDescription = ActivityManager.TaskDescription(null, null, newColor)
|
||||
setTaskDescription(taskDescription)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun showError(text: String, error: Throwable) {
|
||||
@ -70,6 +83,8 @@ abstract class BaseActivity<T : BasePresenter<out BaseView>, VB : ViewBinding> :
|
||||
}
|
||||
|
||||
override fun showExpiredCredentialsDialog() {
|
||||
if (!shouldShowDialog(DIALOG_ERROR_BAD_CREDENTIALS)) return
|
||||
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.main_expired_credentials_title)
|
||||
.setMessage(R.string.main_expired_credentials_description)
|
||||
@ -83,6 +98,8 @@ abstract class BaseActivity<T : BasePresenter<out BaseView>, VB : ViewBinding> :
|
||||
}
|
||||
|
||||
override fun showDecryptionFailedDialog() {
|
||||
if (!shouldShowDialog(DIALOG_ERROR_DECRYPTION_FAILED)) return
|
||||
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.main_session_expired)
|
||||
.setMessage(R.string.main_session_relogin)
|
||||
@ -119,4 +136,21 @@ abstract class BaseActivity<T : BasePresenter<out BaseView>, VB : ViewBinding> :
|
||||
protected open fun inject() {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
private fun shouldShowDialog(name: String): Boolean {
|
||||
val lastOpenTime = lastDialogOpenTime[name]
|
||||
val now = Instant.now()
|
||||
|
||||
if (lastOpenTime != null && now.isBefore(lastOpenTime.plusSeconds(1))) {
|
||||
Timber.i("Dialog $name was shown less than a second ago. Skip")
|
||||
return false
|
||||
}
|
||||
lastDialogOpenTime[name] = Instant.now()
|
||||
return true
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val DIALOG_ERROR_BAD_CREDENTIALS = "dialog_error_bad_credentials"
|
||||
private const val DIALOG_ERROR_DECRYPTION_FAILED = "dialog_error_decryption_failed"
|
||||
}
|
||||
}
|
||||
|
@ -34,7 +34,7 @@ open class ErrorHandler @Inject constructor(@ApplicationContext protected val co
|
||||
}
|
||||
|
||||
protected open fun proceed(error: Throwable) {
|
||||
showErrorMessage(context.resources.getErrorString(error), error)
|
||||
showDefaultMessage(error)
|
||||
when (error) {
|
||||
is PasswordChangeRequiredException -> onPasswordChangeRequired(error.redirectUrl)
|
||||
is ScramblerException -> onDecryptionFailed()
|
||||
@ -45,6 +45,10 @@ open class ErrorHandler @Inject constructor(@ApplicationContext protected val co
|
||||
}
|
||||
}
|
||||
|
||||
fun showDefaultMessage(error: Throwable) {
|
||||
showErrorMessage(context.resources.getErrorString(error), error)
|
||||
}
|
||||
|
||||
open fun clear() {
|
||||
showErrorMessage = { _, _ -> }
|
||||
onExpiredCredentials = {}
|
||||
|
@ -1,5 +1,7 @@
|
||||
package io.github.wulkanowy.ui.modules.attendance
|
||||
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Typeface
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
@ -10,6 +12,7 @@ import io.github.wulkanowy.data.db.entities.Attendance
|
||||
import io.github.wulkanowy.data.enums.SentExcuseStatus
|
||||
import io.github.wulkanowy.databinding.ItemAttendanceBinding
|
||||
import io.github.wulkanowy.utils.descriptionRes
|
||||
import io.github.wulkanowy.utils.getThemeAttrColor
|
||||
import io.github.wulkanowy.utils.isExcusableOrNotExcused
|
||||
import javax.inject.Inject
|
||||
|
||||
@ -31,15 +34,43 @@ class AttendanceAdapter @Inject constructor() :
|
||||
)
|
||||
|
||||
override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
|
||||
val context = holder.binding.root.context
|
||||
val item = items[position]
|
||||
|
||||
with(holder.binding) {
|
||||
attendanceItemNumber.text = item.number.toString()
|
||||
attendanceItemSubject.text = item.subject.ifBlank {
|
||||
root.context.getString(R.string.all_no_data)
|
||||
}
|
||||
attendanceItemSubject.text = item.subject
|
||||
.ifBlank { context.getString(R.string.all_no_data) }
|
||||
attendanceItemDescription.setText(item.descriptionRes)
|
||||
attendanceItemAlert.isVisible = item.let { it.absence && !it.excused }
|
||||
|
||||
attendanceItemDescription.setTextColor(
|
||||
context.getThemeAttrColor(
|
||||
when {
|
||||
item.absence && !item.excused -> R.attr.colorAttendanceAbsence
|
||||
item.lateness && !item.excused -> R.attr.colorAttendanceLateness
|
||||
else -> android.R.attr.textColorSecondary
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
if (item.exemption || item.excused) {
|
||||
attendanceItemDescription.setTypeface(null, Typeface.BOLD)
|
||||
} else {
|
||||
attendanceItemDescription.setTypeface(null, Typeface.NORMAL)
|
||||
}
|
||||
|
||||
attendanceItemAlert.isVisible =
|
||||
item.let { (it.absence && !it.excused) || (it.lateness && !it.excused) }
|
||||
|
||||
attendanceItemAlert.imageTintList = ColorStateList.valueOf(
|
||||
context.getThemeAttrColor(
|
||||
when {
|
||||
item.absence && !item.excused -> R.attr.colorAttendanceAbsence
|
||||
item.lateness && !item.excused -> R.attr.colorAttendanceLateness
|
||||
else -> android.R.attr.colorPrimary
|
||||
}
|
||||
)
|
||||
)
|
||||
attendanceItemNumber.visibility = View.GONE
|
||||
attendanceItemExcuseInfo.visibility = View.GONE
|
||||
attendanceItemExcuseCheckbox.visibility = View.GONE
|
||||
@ -54,10 +85,12 @@ class AttendanceAdapter @Inject constructor() :
|
||||
attendanceItemExcuseInfo.visibility = View.VISIBLE
|
||||
attendanceItemAlert.visibility = View.INVISIBLE
|
||||
}
|
||||
|
||||
SentExcuseStatus.DENIED -> {
|
||||
attendanceItemExcuseInfo.setImageResource(R.drawable.ic_excuse_denied)
|
||||
attendanceItemExcuseInfo.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
else -> {
|
||||
if (item.isExcusableOrNotExcused && excuseActionMode) {
|
||||
attendanceItemNumber.visibility = View.GONE
|
||||
|
@ -6,10 +6,12 @@ import android.view.View
|
||||
import androidx.core.os.bundleOf
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import io.github.wulkanowy.R
|
||||
import io.github.wulkanowy.data.db.entities.Attendance
|
||||
import io.github.wulkanowy.databinding.DialogAttendanceBinding
|
||||
import io.github.wulkanowy.ui.base.BaseDialogFragment
|
||||
import io.github.wulkanowy.utils.descriptionRes
|
||||
import io.github.wulkanowy.utils.getThemeAttrColor
|
||||
import io.github.wulkanowy.utils.serializable
|
||||
import io.github.wulkanowy.utils.toFormattedString
|
||||
|
||||
@ -44,6 +46,16 @@ class AttendanceDialog : BaseDialogFragment<DialogAttendanceBinding>() {
|
||||
with(binding) {
|
||||
attendanceDialogSubjectValue.text = attendance.subject
|
||||
attendanceDialogDescriptionValue.setText(attendance.descriptionRes)
|
||||
attendanceDialogDescriptionValue.setTextColor(
|
||||
root.context.getThemeAttrColor(
|
||||
when {
|
||||
attendance.absence && !attendance.excused -> R.attr.colorAttendanceAbsence
|
||||
attendance.lateness && !attendance.excused -> R.attr.colorAttendanceLateness
|
||||
else -> android.R.attr.textColorSecondary
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
attendanceDialogDateValue.text = attendance.date.toFormattedString()
|
||||
attendanceDialogNumberValue.text = attendance.number.toString()
|
||||
attendanceDialogClose.setOnClickListener { dismiss() }
|
||||
|
@ -14,6 +14,7 @@ import io.github.wulkanowy.data.db.entities.Attendance
|
||||
import io.github.wulkanowy.databinding.DialogExcuseBinding
|
||||
import io.github.wulkanowy.databinding.FragmentAttendanceBinding
|
||||
import io.github.wulkanowy.ui.base.BaseFragment
|
||||
import io.github.wulkanowy.ui.modules.attendance.calculator.AttendanceCalculatorFragment
|
||||
import io.github.wulkanowy.ui.modules.attendance.summary.AttendanceSummaryFragment
|
||||
import io.github.wulkanowy.ui.modules.main.MainActivity
|
||||
import io.github.wulkanowy.ui.modules.main.MainView
|
||||
@ -134,6 +135,7 @@ class AttendanceFragment : BaseFragment<FragmentAttendanceBinding>(R.layout.frag
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return if (item.itemId == R.id.attendanceMenuSummary) presenter.onSummarySwitchSelected()
|
||||
else if (item.itemId == R.id.attendanceMenuCalculator) presenter.onCalculatorSwitchSelected()
|
||||
else false
|
||||
}
|
||||
|
||||
@ -253,6 +255,10 @@ class AttendanceFragment : BaseFragment<FragmentAttendanceBinding>(R.layout.frag
|
||||
(activity as? MainActivity)?.pushView(AttendanceSummaryFragment.newInstance())
|
||||
}
|
||||
|
||||
override fun openCalculatorView() {
|
||||
(activity as? MainActivity)?.pushView(AttendanceCalculatorFragment.newInstance())
|
||||
}
|
||||
|
||||
override fun startActionMode() {
|
||||
actionMode = (activity as MainActivity?)?.startSupportActionMode(actionModeCallback)
|
||||
}
|
||||
|
@ -1,21 +1,37 @@
|
||||
package io.github.wulkanowy.ui.modules.attendance
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import io.github.wulkanowy.data.*
|
||||
import io.github.wulkanowy.data.Resource
|
||||
import io.github.wulkanowy.data.db.entities.Attendance
|
||||
import io.github.wulkanowy.data.db.entities.Semester
|
||||
import io.github.wulkanowy.data.db.entities.Student
|
||||
import io.github.wulkanowy.data.db.entities.Timetable
|
||||
import io.github.wulkanowy.data.flatResourceFlow
|
||||
import io.github.wulkanowy.data.logResourceStatus
|
||||
import io.github.wulkanowy.data.mapResourceData
|
||||
import io.github.wulkanowy.data.onResourceData
|
||||
import io.github.wulkanowy.data.onResourceError
|
||||
import io.github.wulkanowy.data.onResourceIntermediate
|
||||
import io.github.wulkanowy.data.onResourceLoading
|
||||
import io.github.wulkanowy.data.onResourceNotLoading
|
||||
import io.github.wulkanowy.data.onResourceSuccess
|
||||
import io.github.wulkanowy.data.repositories.AttendanceRepository
|
||||
import io.github.wulkanowy.data.repositories.PreferencesRepository
|
||||
import io.github.wulkanowy.data.repositories.SemesterRepository
|
||||
import io.github.wulkanowy.data.repositories.StudentRepository
|
||||
import io.github.wulkanowy.data.repositories.TimetableRepository
|
||||
import io.github.wulkanowy.data.resourceFlow
|
||||
import io.github.wulkanowy.ui.base.BasePresenter
|
||||
import io.github.wulkanowy.ui.base.ErrorHandler
|
||||
import io.github.wulkanowy.utils.*
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import io.github.wulkanowy.utils.AnalyticsHelper
|
||||
import io.github.wulkanowy.utils.capitalise
|
||||
import io.github.wulkanowy.utils.getLastSchoolDayIfHoliday
|
||||
import io.github.wulkanowy.utils.isExcusableOrNotExcused
|
||||
import io.github.wulkanowy.utils.isHolidays
|
||||
import io.github.wulkanowy.utils.monday
|
||||
import io.github.wulkanowy.utils.nextSchoolDay
|
||||
import io.github.wulkanowy.utils.previousOrSameSchoolDay
|
||||
import io.github.wulkanowy.utils.previousSchoolDay
|
||||
import io.github.wulkanowy.utils.sunday
|
||||
import io.github.wulkanowy.utils.toFormattedString
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import timber.log.Timber
|
||||
import java.time.DayOfWeek
|
||||
@ -199,6 +215,11 @@ class AttendancePresenter @Inject constructor(
|
||||
return true
|
||||
}
|
||||
|
||||
fun onCalculatorSwitchSelected(): Boolean {
|
||||
view?.openCalculatorView()
|
||||
return true
|
||||
}
|
||||
|
||||
private fun loadData(forceRefresh: Boolean = false) {
|
||||
Timber.i("Loading attendance data started")
|
||||
|
||||
@ -210,7 +231,7 @@ class AttendancePresenter @Inject constructor(
|
||||
|
||||
val semester = semesterRepository.getCurrentSemester(student)
|
||||
|
||||
checkInitialAndCurrentDate(student, semester)
|
||||
checkInitialAndCurrentDate(semester)
|
||||
attendanceRepository.getAttendance(
|
||||
student = student,
|
||||
semester = semester,
|
||||
@ -266,15 +287,13 @@ class AttendancePresenter @Inject constructor(
|
||||
.launch()
|
||||
}
|
||||
|
||||
private suspend fun checkInitialAndCurrentDate(student: Student, semester: Semester) {
|
||||
private suspend fun checkInitialAndCurrentDate(semester: Semester) {
|
||||
if (initialDate == null) {
|
||||
val lessons = attendanceRepository.getAttendance(
|
||||
student = student,
|
||||
val lessons = attendanceRepository.getAttendanceFromDatabase(
|
||||
semester = semester,
|
||||
start = now().monday,
|
||||
end = now().sunday,
|
||||
forceRefresh = false,
|
||||
).toFirstResult().dataOrNull.orEmpty()
|
||||
).firstOrNull().orEmpty()
|
||||
isWeekendHasLessons = isWeekendHasLessons(lessons)
|
||||
initialDate = getInitialDate(semester)
|
||||
}
|
||||
@ -316,6 +335,7 @@ class AttendancePresenter @Inject constructor(
|
||||
showContent(false)
|
||||
showExcuseButton(false)
|
||||
}
|
||||
|
||||
is Resource.Success -> {
|
||||
Timber.i("Excusing for absence result: Success")
|
||||
analytics.logEvent("excuse_absence", "items" to attendanceToExcuseList.size)
|
||||
@ -328,6 +348,7 @@ class AttendancePresenter @Inject constructor(
|
||||
}
|
||||
loadData(forceRefresh = true)
|
||||
}
|
||||
|
||||
is Resource.Error -> {
|
||||
Timber.i("Excusing for absence result: An exception occurred")
|
||||
errorHandler.dispatch(it.error)
|
||||
|
@ -56,6 +56,8 @@ interface AttendanceView : BaseView {
|
||||
|
||||
fun openSummaryView()
|
||||
|
||||
fun openCalculatorView()
|
||||
|
||||
fun startSendMessageIntent(date: LocalDate, numbers: String, reason: String)
|
||||
|
||||
fun startActionMode()
|
||||
|
@ -0,0 +1,67 @@
|
||||
package io.github.wulkanowy.ui.modules.attendance.calculator
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.github.wulkanowy.R
|
||||
import io.github.wulkanowy.data.pojos.AttendanceData
|
||||
import io.github.wulkanowy.databinding.ItemAttendanceCalculatorHeaderBinding
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class AttendanceCalculatorAdapter @Inject constructor() :
|
||||
RecyclerView.Adapter<AttendanceCalculatorAdapter.ViewHolder>() {
|
||||
|
||||
var items = emptyList<AttendanceData>()
|
||||
|
||||
override fun getItemCount() = items.size
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
|
||||
ItemAttendanceCalculatorHeaderBinding.inflate(
|
||||
LayoutInflater.from(parent.context), parent, false
|
||||
)
|
||||
)
|
||||
|
||||
override fun onBindViewHolder(parent: ViewHolder, position: Int) {
|
||||
val context = parent.binding.root.context
|
||||
val item = items[position]
|
||||
|
||||
with(parent.binding) {
|
||||
attendanceCalculatorPercentage.text = "${item.presencePercentage.roundToInt()}"
|
||||
|
||||
attendanceCalculatorSummaryBalance.text = when {
|
||||
item.lessonBalance > 0 -> {
|
||||
context.getString(
|
||||
R.string.attendance_calculator_summary_balance_positive,
|
||||
item.lessonBalance
|
||||
)
|
||||
}
|
||||
|
||||
item.lessonBalance < 0 -> {
|
||||
context.getString(
|
||||
R.string.attendance_calculator_summary_balance_negative,
|
||||
abs(item.lessonBalance)
|
||||
)
|
||||
}
|
||||
|
||||
else -> context.getString(R.string.attendance_calculator_summary_balance_neutral)
|
||||
}
|
||||
attendanceCalculatorWarning.isVisible = item.lessonBalance < 0
|
||||
attendanceCalculatorTitle.text = item.subjectName
|
||||
attendanceCalculatorSummaryValues.text = if (item.total == 0) {
|
||||
context.getString(R.string.attendance_calculator_summary_values_empty)
|
||||
} else {
|
||||
context.getString(
|
||||
R.string.attendance_calculator_summary_values,
|
||||
item.presences,
|
||||
item.total
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ViewHolder(val binding: ItemAttendanceCalculatorHeaderBinding) :
|
||||
RecyclerView.ViewHolder(binding.root)
|
||||
}
|
@ -0,0 +1,105 @@
|
||||
package io.github.wulkanowy.ui.modules.attendance.calculator
|
||||
|
||||
import android.os.Bundle
|
||||
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.MainView
|
||||
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()
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
binding = FragmentAttendanceCalculatorBinding.bind(view)
|
||||
messageContainer = binding.attendanceCalculatorRecycler
|
||||
presenter.onAttachView(this)
|
||||
}
|
||||
|
||||
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,84 @@
|
||||
package io.github.wulkanowy.ui.modules.attendance.calculator
|
||||
|
||||
import io.github.wulkanowy.data.*
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
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()
|
||||
}
|
@ -78,4 +78,9 @@ class AuthDialog : BaseDialogFragment<DialogAuthBinding>(), AuthView {
|
||||
override fun showDescriptionWithName(name: String) {
|
||||
binding.authDescription.text = getString(R.string.auth_description, name).parseAsHtml()
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
presenter.onDetachView()
|
||||
super.onDestroyView()
|
||||
}
|
||||
}
|
||||
|
@ -62,7 +62,11 @@ class AuthPresenter @Inject constructor(
|
||||
}
|
||||
isSuccess
|
||||
}
|
||||
.onFailure { errorHandler.dispatch(it) }
|
||||
.onFailure {
|
||||
errorHandler.dispatch(it)
|
||||
view?.showProgress(false)
|
||||
view?.showContent(true)
|
||||
}
|
||||
.onSuccess {
|
||||
if (it) {
|
||||
view?.showSuccess(true)
|
||||
|
@ -10,9 +10,10 @@ import android.webkit.WebViewClient
|
||||
import androidx.core.os.bundleOf
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import io.github.wulkanowy.R
|
||||
import io.github.wulkanowy.data.WulkanowySdkFactory
|
||||
import io.github.wulkanowy.databinding.DialogCaptchaBinding
|
||||
import io.github.wulkanowy.sdk.Sdk
|
||||
import io.github.wulkanowy.ui.base.BaseDialogFragment
|
||||
import io.github.wulkanowy.utils.WebkitCookieManagerProxy
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@ -20,7 +21,10 @@ import javax.inject.Inject
|
||||
class CaptchaDialog : BaseDialogFragment<DialogCaptchaBinding>() {
|
||||
|
||||
@Inject
|
||||
lateinit var sdk: Sdk
|
||||
lateinit var wulkanowySdkFactory: WulkanowySdkFactory
|
||||
|
||||
@Inject
|
||||
lateinit var webkitCookieManagerProxy: WebkitCookieManagerProxy
|
||||
|
||||
private var webView: WebView? = null
|
||||
|
||||
@ -55,7 +59,7 @@ class CaptchaDialog : BaseDialogFragment<DialogCaptchaBinding>() {
|
||||
webView = this
|
||||
with(settings) {
|
||||
javaScriptEnabled = true
|
||||
userAgentString = sdk.userAgent
|
||||
userAgentString = wulkanowySdkFactory.create().userAgent
|
||||
}
|
||||
|
||||
webViewClient = object : WebViewClient() {
|
||||
@ -80,6 +84,7 @@ class CaptchaDialog : BaseDialogFragment<DialogCaptchaBinding>() {
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
webkitCookieManagerProxy.webkitCookieManager?.flush()
|
||||
webView?.destroy()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
@ -24,11 +24,13 @@ import io.github.wulkanowy.data.repositories.SemesterRepository
|
||||
import io.github.wulkanowy.data.repositories.StudentRepository
|
||||
import io.github.wulkanowy.data.repositories.TimetableRepository
|
||||
import io.github.wulkanowy.domain.adminmessage.GetAppropriateAdminMessageUseCase
|
||||
import io.github.wulkanowy.domain.timetable.IsStudentHasLessonsOnWeekendUseCase
|
||||
import io.github.wulkanowy.ui.base.BasePresenter
|
||||
import io.github.wulkanowy.ui.base.ErrorHandler
|
||||
import io.github.wulkanowy.utils.AdsHelper
|
||||
import io.github.wulkanowy.utils.calculatePercentage
|
||||
import io.github.wulkanowy.utils.nextOrSameSchoolDay
|
||||
import io.github.wulkanowy.utils.sunday
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.combine
|
||||
@ -56,6 +58,7 @@ class DashboardPresenter @Inject constructor(
|
||||
private val messageRepository: MessageRepository,
|
||||
private val attendanceSummaryRepository: AttendanceSummaryRepository,
|
||||
private val timetableRepository: TimetableRepository,
|
||||
private val isStudentHasLessonsOnWeekendUseCase: IsStudentHasLessonsOnWeekendUseCase,
|
||||
private val homeworkRepository: HomeworkRepository,
|
||||
private val examRepository: ExamRepository,
|
||||
private val conferenceRepository: ConferenceRepository,
|
||||
@ -301,6 +304,7 @@ class DashboardPresenter @Inject constructor(
|
||||
forceRefresh = forceRefresh
|
||||
)
|
||||
}
|
||||
.mapResourceData { it.map { messageWithAuthor -> messageWithAuthor.message } }
|
||||
.onResourceError { errorHandler.dispatch(it) }
|
||||
.takeIf { DashboardItem.Tile.MESSAGES in selectedTiles } ?: flowSuccess
|
||||
|
||||
@ -435,14 +439,17 @@ class DashboardPresenter @Inject constructor(
|
||||
private fun loadLessons(student: Student, forceRefresh: Boolean) {
|
||||
flatResourceFlow {
|
||||
val semester = semesterRepository.getCurrentSemester(student)
|
||||
val date = LocalDate.now().nextOrSameSchoolDay
|
||||
val date = when (isStudentHasLessonsOnWeekendUseCase(semester)) {
|
||||
true -> LocalDate.now()
|
||||
else -> LocalDate.now().nextOrSameSchoolDay
|
||||
}
|
||||
|
||||
timetableRepository.getTimetable(
|
||||
student = student,
|
||||
semester = semester,
|
||||
start = date,
|
||||
end = date,
|
||||
forceRefresh = forceRefresh
|
||||
end = date.sunday,
|
||||
forceRefresh = forceRefresh,
|
||||
)
|
||||
}
|
||||
.onEach {
|
||||
|
@ -18,6 +18,7 @@ import io.github.wulkanowy.ui.base.ErrorHandler
|
||||
import io.github.wulkanowy.ui.modules.debug.notification.mock.debugAttendanceItems
|
||||
import io.github.wulkanowy.ui.modules.debug.notification.mock.debugConferenceItems
|
||||
import io.github.wulkanowy.ui.modules.debug.notification.mock.debugExamItems
|
||||
import io.github.wulkanowy.ui.modules.debug.notification.mock.debugGradeDescriptiveItems
|
||||
import io.github.wulkanowy.ui.modules.debug.notification.mock.debugGradeDetailsItems
|
||||
import io.github.wulkanowy.ui.modules.debug.notification.mock.debugGradeSummaryItems
|
||||
import io.github.wulkanowy.ui.modules.debug.notification.mock.debugHomeworkItems
|
||||
@ -55,6 +56,14 @@ class NotificationDebugPresenter @Inject constructor(
|
||||
NotificationDebugItem(R.string.grade_summary_final_grade) { n ->
|
||||
withStudent { newGradeNotification.notifyFinal(debugGradeSummaryItems.take(n), it) }
|
||||
},
|
||||
NotificationDebugItem(R.string.grade_summary_descriptive) { n ->
|
||||
withStudent {
|
||||
newGradeNotification.notifyDescriptive(
|
||||
debugGradeDescriptiveItems.take(n),
|
||||
it
|
||||
)
|
||||
}
|
||||
},
|
||||
NotificationDebugItem(R.string.homework_title) { n ->
|
||||
withStudent { newHomeworkNotification.notify(debugHomeworkItems.take(n), it) }
|
||||
},
|
||||
|
@ -0,0 +1,48 @@
|
||||
package io.github.wulkanowy.ui.modules.debug.notification.mock
|
||||
|
||||
import io.github.wulkanowy.data.db.entities.GradeDescriptive
|
||||
|
||||
val debugGradeDescriptiveItems = listOf(
|
||||
generateGradeDescriptive(
|
||||
"Matematyka",
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit."
|
||||
),
|
||||
generateGradeDescriptive("Fizyka", "Lorem ipsum dolor sit amet, consectetur adipiscing elit."),
|
||||
generateGradeDescriptive(
|
||||
"Geografia",
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit."
|
||||
),
|
||||
generateGradeDescriptive(
|
||||
"Sieci komputerowe",
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit."
|
||||
),
|
||||
generateGradeDescriptive(
|
||||
"Systemy operacyjne",
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit."
|
||||
),
|
||||
generateGradeDescriptive(
|
||||
"Język polski",
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit."
|
||||
),
|
||||
generateGradeDescriptive(
|
||||
"Język angielski",
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit."
|
||||
),
|
||||
generateGradeDescriptive("Religia", "Lorem ipsum dolor sit amet, consectetur adipiscing elit."),
|
||||
generateGradeDescriptive(
|
||||
"Język niemiecki",
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit."
|
||||
),
|
||||
generateGradeDescriptive(
|
||||
"Wychowanie fizyczne",
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit."
|
||||
),
|
||||
)
|
||||
|
||||
private fun generateGradeDescriptive(subject: String, description: String) =
|
||||
GradeDescriptive(
|
||||
semesterId = 0,
|
||||
studentId = 0,
|
||||
subject = subject,
|
||||
description = description
|
||||
)
|
@ -1,15 +1,23 @@
|
||||
package io.github.wulkanowy.ui.modules.grade
|
||||
|
||||
import io.github.wulkanowy.data.*
|
||||
import io.github.wulkanowy.data.Resource
|
||||
import io.github.wulkanowy.data.dataOrNull
|
||||
import io.github.wulkanowy.data.db.entities.Grade
|
||||
import io.github.wulkanowy.data.db.entities.GradeDescriptive
|
||||
import io.github.wulkanowy.data.db.entities.GradeSummary
|
||||
import io.github.wulkanowy.data.db.entities.Semester
|
||||
import io.github.wulkanowy.data.db.entities.Student
|
||||
import io.github.wulkanowy.data.errorOrNull
|
||||
import io.github.wulkanowy.data.flatResourceFlow
|
||||
import io.github.wulkanowy.data.mapData
|
||||
import io.github.wulkanowy.data.mapResourceData
|
||||
import io.github.wulkanowy.data.repositories.GradeRepository
|
||||
import io.github.wulkanowy.data.repositories.PreferencesRepository
|
||||
import io.github.wulkanowy.data.repositories.SemesterRepository
|
||||
import io.github.wulkanowy.sdk.Sdk
|
||||
import io.github.wulkanowy.ui.modules.grade.GradeAverageMode.*
|
||||
import io.github.wulkanowy.ui.modules.grade.GradeAverageMode.ALL_YEAR
|
||||
import io.github.wulkanowy.ui.modules.grade.GradeAverageMode.BOTH_SEMESTERS
|
||||
import io.github.wulkanowy.ui.modules.grade.GradeAverageMode.ONE_SEMESTER
|
||||
import io.github.wulkanowy.utils.calcAverage
|
||||
import io.github.wulkanowy.utils.changeModifier
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
@ -62,6 +70,7 @@ class GradeAverageProvider @Inject constructor(
|
||||
forceRefresh = forceRefresh,
|
||||
params = params,
|
||||
)
|
||||
|
||||
BOTH_SEMESTERS -> calculateCombinedAverage(
|
||||
student = student,
|
||||
semesters = semesters,
|
||||
@ -69,6 +78,7 @@ class GradeAverageProvider @Inject constructor(
|
||||
forceRefresh = forceRefresh,
|
||||
config = params,
|
||||
)
|
||||
|
||||
ALL_YEAR -> calculateCombinedAverage(
|
||||
student = student,
|
||||
semesters = semesters,
|
||||
@ -149,7 +159,7 @@ class GradeAverageProvider @Inject constructor(
|
||||
?.updateModifiers(student, config).orEmpty()
|
||||
|
||||
(updatedSecondSemesterGrades + updatedFirstSemesterGrades).calcAverage(
|
||||
config.isOptionalArithmeticAverage
|
||||
isOptionalArithmeticAverage = config.isOptionalArithmeticAverage,
|
||||
)
|
||||
} else {
|
||||
secondSemesterSubject.average
|
||||
@ -163,13 +173,21 @@ class GradeAverageProvider @Inject constructor(
|
||||
config: AverageCalcParams,
|
||||
): Double {
|
||||
return if (!isAnyVulcanAverage || config.forceAverageCalc) {
|
||||
val divider = if (secondSemesterSubject.grades.any { it.weightValue > .0 }) 2 else 1
|
||||
val isSecondSemesterHasWeightGrade = secondSemesterSubject.grades
|
||||
.any { it.weightValue > .0 }
|
||||
val isSecondSemesterHasArithmeticGrade = secondSemesterSubject.grades
|
||||
.all { it.weightValue == .0 } && config.isOptionalArithmeticAverage
|
||||
val isSecondSemesterHaveAverage =
|
||||
isSecondSemesterHasWeightGrade || isSecondSemesterHasArithmeticGrade
|
||||
|
||||
val divider = if (isSecondSemesterHaveAverage) 2 else 1
|
||||
val secondSemesterAverage = secondSemesterSubject.grades
|
||||
.updateModifiers(student, config)
|
||||
.calcAverage(config.isOptionalArithmeticAverage)
|
||||
.calcAverage(isOptionalArithmeticAverage = config.isOptionalArithmeticAverage)
|
||||
val firstSemesterAverage = firstSemesterSubject?.grades
|
||||
?.updateModifiers(student, config)
|
||||
?.calcAverage(config.isOptionalArithmeticAverage) ?: secondSemesterAverage
|
||||
?.calcAverage(isOptionalArithmeticAverage = config.isOptionalArithmeticAverage)
|
||||
?: secondSemesterAverage
|
||||
|
||||
(secondSemesterAverage + firstSemesterAverage) / divider
|
||||
} else {
|
||||
@ -189,36 +207,73 @@ class GradeAverageProvider @Inject constructor(
|
||||
): Flow<Resource<List<GradeSubject>>> {
|
||||
return gradeRepository.getGrades(student, semester, forceRefresh = forceRefresh)
|
||||
.mapResourceData { res ->
|
||||
val (details, summaries) = res
|
||||
val (details, summaries, descriptives) = res
|
||||
val isAnyAverage = summaries.any { it.average != .0 }
|
||||
val allGrades = details.groupBy { it.subject }
|
||||
val descriptiveGradesBySubject = descriptives.associateBy { it.subject }
|
||||
|
||||
val items = summaries.emulateEmptySummaries(
|
||||
student = student,
|
||||
semester = semester,
|
||||
grades = allGrades.toList(),
|
||||
calcAverage = isAnyAverage,
|
||||
params = params,
|
||||
).map { summary ->
|
||||
val grades = allGrades[summary.subject].orEmpty()
|
||||
GradeSubject(
|
||||
subject = summary.subject,
|
||||
average = if (!isAnyAverage || params.forceAverageCalc) {
|
||||
grades.updateModifiers(student, params)
|
||||
.calcAverage(params.isOptionalArithmeticAverage)
|
||||
} else summary.average,
|
||||
points = summary.pointsSum,
|
||||
summary = summary,
|
||||
grades = grades,
|
||||
isVulcanAverage = isAnyAverage
|
||||
val items = summaries
|
||||
.createEmptySummariesByGradesIfNeeded(
|
||||
student = student,
|
||||
semester = semester,
|
||||
grades = allGrades.toList(),
|
||||
calcAverage = isAnyAverage,
|
||||
params = params,
|
||||
)
|
||||
}
|
||||
.createEmptySummariesByDescriptiveGradesIfNeeded(
|
||||
student = student,
|
||||
semester = semester,
|
||||
descriptives = descriptives,
|
||||
)
|
||||
.map { summary ->
|
||||
val grades = allGrades[summary.subject].orEmpty()
|
||||
val descriptiveGrade = descriptiveGradesBySubject[summary.subject]
|
||||
|
||||
GradeSubject(
|
||||
subject = summary.subject,
|
||||
average = if (!isAnyAverage || params.forceAverageCalc) {
|
||||
grades.updateModifiers(student, params)
|
||||
.calcAverage(isOptionalArithmeticAverage = params.isOptionalArithmeticAverage)
|
||||
} else summary.average,
|
||||
points = summary.pointsSum,
|
||||
summary = summary,
|
||||
grades = grades,
|
||||
descriptive = descriptiveGrade,
|
||||
isVulcanAverage = isAnyAverage
|
||||
)
|
||||
}
|
||||
|
||||
items
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<GradeSummary>.emulateEmptySummaries(
|
||||
private fun List<GradeSummary>.createEmptySummariesByDescriptiveGradesIfNeeded(
|
||||
student: Student,
|
||||
semester: Semester,
|
||||
descriptives: List<GradeDescriptive>
|
||||
): List<GradeSummary> {
|
||||
val summarySubjects = this.map { it.subject }
|
||||
val gradeSummaryToAdd = descriptives.mapNotNull { gradeDescriptive ->
|
||||
if (gradeDescriptive.subject in summarySubjects) return@mapNotNull null
|
||||
|
||||
GradeSummary(
|
||||
studentId = student.studentId,
|
||||
semesterId = semester.semesterId,
|
||||
position = 0,
|
||||
subject = gradeDescriptive.subject,
|
||||
predictedGrade = "",
|
||||
finalGrade = "",
|
||||
proposedPoints = "",
|
||||
finalPoints = "",
|
||||
pointsSum = "",
|
||||
average = .0
|
||||
)
|
||||
}
|
||||
|
||||
return this + gradeSummaryToAdd
|
||||
}
|
||||
|
||||
private fun List<GradeSummary>.createEmptySummariesByGradesIfNeeded(
|
||||
student: Student,
|
||||
semester: Semester,
|
||||
grades: List<Pair<String, List<Grade>>>,
|
||||
@ -239,8 +294,13 @@ class GradeAverageProvider @Inject constructor(
|
||||
proposedPoints = "",
|
||||
finalPoints = "",
|
||||
pointsSum = "",
|
||||
average = if (calcAverage) details.updateModifiers(student, params)
|
||||
.calcAverage(params.isOptionalArithmeticAverage) else .0
|
||||
average = when {
|
||||
calcAverage -> details
|
||||
.updateModifiers(student, params)
|
||||
.calcAverage(isOptionalArithmeticAverage = params.isOptionalArithmeticAverage)
|
||||
|
||||
else -> .0
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package io.github.wulkanowy.ui.modules.grade
|
||||
|
||||
import io.github.wulkanowy.data.db.entities.Grade
|
||||
import io.github.wulkanowy.data.db.entities.GradeDescriptive
|
||||
import io.github.wulkanowy.data.db.entities.GradeSummary
|
||||
|
||||
data class GradeSubject(
|
||||
@ -8,6 +9,7 @@ data class GradeSubject(
|
||||
val average: Double,
|
||||
val points: String,
|
||||
val summary: GradeSummary,
|
||||
val descriptive: GradeDescriptive?,
|
||||
val grades: List<Grade>,
|
||||
val isVulcanAverage: Boolean
|
||||
)
|
||||
|
@ -1,13 +1,22 @@
|
||||
package io.github.wulkanowy.ui.modules.grade.details
|
||||
|
||||
import io.github.wulkanowy.data.*
|
||||
import io.github.wulkanowy.data.db.entities.Grade
|
||||
import io.github.wulkanowy.data.enums.GradeExpandMode
|
||||
import io.github.wulkanowy.data.enums.GradeSortingMode.*
|
||||
import io.github.wulkanowy.data.enums.GradeSortingMode.ALPHABETIC
|
||||
import io.github.wulkanowy.data.enums.GradeSortingMode.AVERAGE
|
||||
import io.github.wulkanowy.data.enums.GradeSortingMode.DATE
|
||||
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.onResourceSuccess
|
||||
import io.github.wulkanowy.data.repositories.GradeRepository
|
||||
import io.github.wulkanowy.data.repositories.PreferencesRepository
|
||||
import io.github.wulkanowy.data.repositories.SemesterRepository
|
||||
import io.github.wulkanowy.data.repositories.StudentRepository
|
||||
import io.github.wulkanowy.data.resourceFlow
|
||||
import io.github.wulkanowy.ui.base.BasePresenter
|
||||
import io.github.wulkanowy.ui.base.ErrorHandler
|
||||
import io.github.wulkanowy.ui.modules.grade.GradeAverageProvider
|
||||
@ -207,20 +216,20 @@ class GradeDetailsPresenter @Inject constructor(
|
||||
AVERAGE -> gradeSubjects.sortedByDescending { it.average }
|
||||
}
|
||||
}
|
||||
.map { (subject, average, points, _, grades) ->
|
||||
val subItems = grades
|
||||
.map { gradeSubject ->
|
||||
val subItems = gradeSubject.grades
|
||||
.sortedByDescending { it.date }
|
||||
.map { GradeDetailsItem(it, ViewType.ITEM) }
|
||||
|
||||
val gradeDetailsItems = listOf(
|
||||
GradeDetailsItem(
|
||||
GradeDetailsHeader(
|
||||
subject = subject,
|
||||
average = average,
|
||||
pointsSum = points,
|
||||
subject = gradeSubject.subject,
|
||||
average = gradeSubject.average,
|
||||
pointsSum = gradeSubject.points,
|
||||
grades = subItems
|
||||
).apply {
|
||||
newGrades = grades.filter { grade -> !grade.isRead }.size
|
||||
newGrades = gradeSubject.grades.filter { grade -> !grade.isRead }.size
|
||||
}, ViewType.HEADER
|
||||
)
|
||||
)
|
||||
|
@ -2,16 +2,16 @@ package io.github.wulkanowy.ui.modules.grade.summary
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.github.wulkanowy.R
|
||||
import io.github.wulkanowy.data.db.entities.GradeSummary
|
||||
import io.github.wulkanowy.data.repositories.PreferencesRepository
|
||||
import io.github.wulkanowy.databinding.ItemGradeSummaryBinding
|
||||
import io.github.wulkanowy.databinding.ScrollableHeaderGradeSummaryBinding
|
||||
import io.github.wulkanowy.sdk.scrapper.grades.isGradeValid
|
||||
import io.github.wulkanowy.utils.calcFinalAverage
|
||||
import io.github.wulkanowy.utils.ifNullOrBlank
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
|
||||
@ -24,7 +24,7 @@ class GradeSummaryAdapter @Inject constructor(
|
||||
ITEM(2)
|
||||
}
|
||||
|
||||
var items = emptyList<GradeSummary>()
|
||||
var items = emptyList<GradeSummaryItem>()
|
||||
|
||||
var onCalculatedHelpClickListener: () -> Unit = {}
|
||||
|
||||
@ -44,9 +44,11 @@ class GradeSummaryAdapter @Inject constructor(
|
||||
ViewType.HEADER.id -> HeaderViewHolder(
|
||||
ScrollableHeaderGradeSummaryBinding.inflate(inflater, parent, false)
|
||||
)
|
||||
|
||||
ViewType.ITEM.id -> ItemViewHolder(
|
||||
ItemGradeSummaryBinding.inflate(inflater, parent, false)
|
||||
)
|
||||
|
||||
else -> throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
@ -60,19 +62,23 @@ class GradeSummaryAdapter @Inject constructor(
|
||||
|
||||
private fun bindHeaderViewHolder(binding: ScrollableHeaderGradeSummaryBinding) {
|
||||
if (items.isEmpty()) return
|
||||
val gradeSummaries = items
|
||||
.filter { it.gradeDescriptive == null }
|
||||
.map { it.gradeSummary }
|
||||
|
||||
val context = binding.root.context
|
||||
val finalItemsCount = items.count { isGradeValid(it.finalGrade) }
|
||||
val calculatedItemsCount = items.count { value -> value.average != 0.0 }
|
||||
val allItemsCount = items.count { !it.subject.equals("zachowanie", true) }
|
||||
val finalAverage = items.calcFinalAverage(
|
||||
val finalItemsCount = gradeSummaries.count { isGradeValid(it.finalGrade) }
|
||||
val calculatedItemsCount = gradeSummaries.count { value -> value.average != 0.0 }
|
||||
val allItemsCount = gradeSummaries.count { !it.subject.equals("zachowanie", true) }
|
||||
val finalAverage = gradeSummaries.calcFinalAverage(
|
||||
preferencesRepository.gradePlusModifier,
|
||||
preferencesRepository.gradeMinusModifier
|
||||
)
|
||||
val calculatedAverage = items.filter { value -> value.average != 0.0 }
|
||||
val calculatedAverage = gradeSummaries.filter { value -> value.average != 0.0 }
|
||||
.map { values -> values.average }
|
||||
.reversed() // fix average precision
|
||||
.average()
|
||||
.let { if (it.isNaN()) 0.0 else it }
|
||||
|
||||
with(binding) {
|
||||
gradeSummaryScrollableHeaderFinal.text = formatAverage(finalAverage)
|
||||
@ -95,16 +101,28 @@ class GradeSummaryAdapter @Inject constructor(
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
private fun bindItemViewHolder(binding: ItemGradeSummaryBinding, item: GradeSummary) {
|
||||
with(binding) {
|
||||
gradeSummaryItemTitle.text = item.subject
|
||||
gradeSummaryItemPoints.text = item.pointsSum
|
||||
gradeSummaryItemAverage.text = formatAverage(item.average, "")
|
||||
gradeSummaryItemPredicted.text = "${item.predictedGrade} ${item.proposedPoints}".trim()
|
||||
gradeSummaryItemFinal.text = "${item.finalGrade} ${item.finalPoints}".trim()
|
||||
private fun bindItemViewHolder(binding: ItemGradeSummaryBinding, item: GradeSummaryItem) {
|
||||
val (gradeSummary, gradeDescriptive) = item
|
||||
|
||||
gradeSummaryItemPointsContainer.visibility =
|
||||
if (item.pointsSum.isBlank()) View.GONE else View.VISIBLE
|
||||
with(binding) {
|
||||
gradeSummaryItemTitle.text = gradeSummary.subject
|
||||
gradeSummaryItemPoints.text = gradeSummary.pointsSum
|
||||
gradeSummaryItemAverage.text = formatAverage(gradeSummary.average, "")
|
||||
gradeSummaryItemPredicted.text =
|
||||
"${gradeSummary.predictedGrade} ${gradeSummary.proposedPoints}".trim()
|
||||
gradeSummaryItemFinal.text =
|
||||
"${gradeSummary.finalGrade} ${gradeSummary.finalPoints}".trim()
|
||||
gradeSummaryItemDescriptive.text = gradeDescriptive?.description.ifNullOrBlank {
|
||||
root.context.getString(R.string.all_no_data)
|
||||
}
|
||||
|
||||
gradeSummaryItemFinalDivider.isVisible = gradeDescriptive == null
|
||||
gradeSummaryItemPredictedDivider.isVisible = gradeDescriptive == null
|
||||
gradeSummaryItemPointsDivider.isVisible = gradeDescriptive == null
|
||||
gradeSummaryItemPredictedContainer.isVisible = gradeDescriptive == null
|
||||
gradeSummaryItemFinalContainer.isVisible = gradeDescriptive == null
|
||||
gradeSummaryItemDescriptiveContainer.isVisible = gradeDescriptive != null
|
||||
gradeSummaryItemPointsContainer.isVisible = gradeSummary.pointsSum.isNotBlank()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5,12 +5,10 @@ import android.view.View
|
||||
import android.view.View.GONE
|
||||
import android.view.View.INVISIBLE
|
||||
import android.view.View.VISIBLE
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import io.github.wulkanowy.R
|
||||
import io.github.wulkanowy.data.db.entities.GradeSummary
|
||||
import io.github.wulkanowy.databinding.FragmentGradeSummaryBinding
|
||||
import io.github.wulkanowy.ui.base.BaseFragment
|
||||
import io.github.wulkanowy.ui.modules.grade.GradeFragment
|
||||
@ -72,7 +70,7 @@ class GradeSummaryFragment :
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateData(data: List<GradeSummary>) {
|
||||
override fun updateData(data: List<GradeSummaryItem>) {
|
||||
with(gradeSummaryAdapter) {
|
||||
items = data
|
||||
notifyDataSetChanged()
|
||||
|
@ -0,0 +1,9 @@
|
||||
package io.github.wulkanowy.ui.modules.grade.summary
|
||||
|
||||
import io.github.wulkanowy.data.db.entities.GradeDescriptive
|
||||
import io.github.wulkanowy.data.db.entities.GradeSummary
|
||||
|
||||
data class GradeSummaryItem(
|
||||
val gradeSummary: GradeSummary,
|
||||
val gradeDescriptive: GradeDescriptive?
|
||||
)
|
@ -1,9 +1,16 @@
|
||||
package io.github.wulkanowy.ui.modules.grade.summary
|
||||
|
||||
import io.github.wulkanowy.data.*
|
||||
import io.github.wulkanowy.data.db.entities.GradeSummary
|
||||
import io.github.wulkanowy.data.enums.GradeSortingMode
|
||||
import io.github.wulkanowy.data.enums.GradeSortingMode.*
|
||||
import io.github.wulkanowy.data.enums.GradeSortingMode.ALPHABETIC
|
||||
import io.github.wulkanowy.data.enums.GradeSortingMode.AVERAGE
|
||||
import io.github.wulkanowy.data.enums.GradeSortingMode.DATE
|
||||
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.onResourceNotLoading
|
||||
import io.github.wulkanowy.data.onResourceSuccess
|
||||
import io.github.wulkanowy.data.repositories.PreferencesRepository
|
||||
import io.github.wulkanowy.data.repositories.StudentRepository
|
||||
import io.github.wulkanowy.ui.base.BasePresenter
|
||||
@ -128,7 +135,7 @@ class GradeSummaryPresenter @Inject constructor(
|
||||
view?.showFinalAverageHelpDialog()
|
||||
}
|
||||
|
||||
private fun createGradeSummaryItems(items: List<GradeSubject>): List<GradeSummary> {
|
||||
private fun createGradeSummaryItems(items: List<GradeSubject>): List<GradeSummaryItem> {
|
||||
return items
|
||||
.filter { !checkEmpty(it) }
|
||||
.let { gradeSubjects ->
|
||||
@ -136,21 +143,32 @@ class GradeSummaryPresenter @Inject constructor(
|
||||
DATE -> gradeSubjects.sortedByDescending { gradeDetailsWithAverage ->
|
||||
gradeDetailsWithAverage.grades.maxByOrNull { it.date }?.date
|
||||
}
|
||||
|
||||
ALPHABETIC -> gradeSubjects.sortedBy { gradeDetailsWithAverage ->
|
||||
gradeDetailsWithAverage.subject.lowercase()
|
||||
}
|
||||
|
||||
AVERAGE -> gradeSubjects.sortedByDescending { it.average }
|
||||
}
|
||||
}
|
||||
.map { it.summary.copy(average = it.average) }
|
||||
.map {
|
||||
val gradeSummary = it.summary.copy(average = it.average)
|
||||
val descriptive = it.descriptive
|
||||
GradeSummaryItem(
|
||||
gradeSummary = gradeSummary,
|
||||
gradeDescriptive = descriptive,
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun checkEmpty(gradeSummary: GradeSubject): Boolean {
|
||||
return gradeSummary.run {
|
||||
summary.finalGrade.isBlank()
|
||||
&& summary.predictedGrade.isBlank()
|
||||
&& average == .0
|
||||
&& points.isBlank()
|
||||
&& summary.predictedGrade.isBlank()
|
||||
&& average == .0
|
||||
&& points.isBlank()
|
||||
&& descriptive == null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
package io.github.wulkanowy.ui.modules.grade.summary
|
||||
|
||||
import io.github.wulkanowy.data.db.entities.GradeSummary
|
||||
import io.github.wulkanowy.ui.base.BaseView
|
||||
|
||||
interface GradeSummaryView : BaseView {
|
||||
@ -13,7 +12,7 @@ interface GradeSummaryView : BaseView {
|
||||
|
||||
fun initView()
|
||||
|
||||
fun updateData(data: List<GradeSummary>)
|
||||
fun updateData(data: List<GradeSummaryItem>)
|
||||
|
||||
fun resetView()
|
||||
|
||||
|
@ -7,5 +7,6 @@ data class LoginData(
|
||||
val password: String,
|
||||
val baseUrl: String,
|
||||
val domainSuffix: String,
|
||||
val symbol: String?,
|
||||
val defaultSymbol: String,
|
||||
val userEnteredSymbol: String? = null,
|
||||
) : Serializable
|
||||
|
@ -71,7 +71,7 @@ class LoginAdvancedPresenter @Inject constructor(
|
||||
|
||||
fun updateUsernameLabel() {
|
||||
view?.apply {
|
||||
setUsernameLabel(if ("vulcan" in formHostValue || "fakelog" in formHostValue) emailLabel else nicknameLabel)
|
||||
setUsernameLabel(if ("vulcan" in formHostValue || "wulkanowy" in formHostValue) emailLabel else nicknameLabel)
|
||||
}
|
||||
}
|
||||
|
||||
@ -79,7 +79,7 @@ class LoginAdvancedPresenter @Inject constructor(
|
||||
view?.apply {
|
||||
clearPassError()
|
||||
clearUsernameError()
|
||||
if (formHostValue.contains("fakelog")) {
|
||||
if (formHostValue.contains("wulkanowy")) {
|
||||
setDefaultCredentials(
|
||||
"jan@fakelog.cf", "jan123", "powiatwulkanowy", "FK100000", "999999"
|
||||
)
|
||||
@ -155,7 +155,7 @@ class LoginAdvancedPresenter @Inject constructor(
|
||||
password = view?.formPassValue.orEmpty().trim(),
|
||||
baseUrl = view?.formHostValue.orEmpty().trim(),
|
||||
domainSuffix = view?.formDomainSuffix.orEmpty().trim(),
|
||||
symbol = view?.formSymbolValue.orEmpty().trim().getNormalizedSymbol(),
|
||||
defaultSymbol = view?.formSymbolValue.orEmpty().trim().getNormalizedSymbol(),
|
||||
)
|
||||
when (it.data.symbols.size) {
|
||||
0 -> view?.navigateToSymbol(loginData)
|
||||
|
@ -94,6 +94,7 @@ class LoginFormFragment : BaseFragment<FragmentLoginFormBinding>(R.layout.fragme
|
||||
loginFormUsername.doOnTextChanged { _, _, _, _ -> presenter.onUsernameTextChanged() }
|
||||
loginFormPass.doOnTextChanged { _, _, _, _ -> presenter.onPassTextChanged() }
|
||||
loginFormHost.setOnItemClickListener { _, _, _, _ -> presenter.onHostSelected() }
|
||||
loginFormDomainSuffix.doOnTextChanged { _, _, _, _ -> presenter.onDomainSuffixChanged() }
|
||||
loginFormSignIn.setOnClickListener { presenter.onSignInClick() }
|
||||
loginFormAdvancedButton.setOnClickListener { presenter.onAdvancedLoginClick() }
|
||||
loginFormPrivacyLink.setOnClickListener { presenter.onPrivacyLinkClick() }
|
||||
@ -188,6 +189,12 @@ class LoginFormFragment : BaseFragment<FragmentLoginFormBinding>(R.layout.fragme
|
||||
}
|
||||
}
|
||||
|
||||
override fun setDomainSuffixInvalid() {
|
||||
with(binding.loginFormDomainSuffixLayout) {
|
||||
error = getString(R.string.login_invalid_domain_suffix)
|
||||
}
|
||||
}
|
||||
|
||||
override fun clearUsernameError() {
|
||||
binding.loginFormUsernameLayout.error = null
|
||||
binding.loginFormErrorBox.isVisible = false
|
||||
@ -206,6 +213,10 @@ class LoginFormFragment : BaseFragment<FragmentLoginFormBinding>(R.layout.fragme
|
||||
binding.loginFormErrorBox.isVisible = false
|
||||
}
|
||||
|
||||
override fun clearDomainSuffixError() {
|
||||
binding.loginFormDomainSuffixLayout.error = null
|
||||
}
|
||||
|
||||
override fun showSoftKeyboard() {
|
||||
activity?.showSoftInput()
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ import io.github.wulkanowy.data.repositories.PreferencesRepository
|
||||
import io.github.wulkanowy.data.repositories.StudentRepository
|
||||
import io.github.wulkanowy.data.resourceFlow
|
||||
import io.github.wulkanowy.domain.adminmessage.GetAppropriateAdminMessageUseCase
|
||||
import io.github.wulkanowy.sdk.scrapper.login.InvalidSymbolException
|
||||
import io.github.wulkanowy.ui.base.BasePresenter
|
||||
import io.github.wulkanowy.ui.modules.login.LoginData
|
||||
import io.github.wulkanowy.ui.modules.login.LoginErrorHandler
|
||||
@ -90,7 +91,7 @@ class LoginFormPresenter @Inject constructor(
|
||||
clearPassError()
|
||||
clearUsernameError()
|
||||
clearHostError()
|
||||
if (formHostValue.contains("fakelog")) {
|
||||
if (formHostValue.contains("wulkanowy")) {
|
||||
setCredentials("jan@fakelog.cf", "jan123")
|
||||
} else if (formUsernameValue == "jan@fakelog.cf" && formPassValue == "jan123") {
|
||||
setCredentials("", "")
|
||||
@ -101,6 +102,12 @@ class LoginFormPresenter @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun onDomainSuffixChanged() {
|
||||
view?.apply {
|
||||
clearDomainSuffixError()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateCustomDomainSuffixVisibility() {
|
||||
view?.run {
|
||||
showDomainSuffixInput("customSuffix" in formHostValue)
|
||||
@ -148,7 +155,7 @@ class LoginFormPresenter @Inject constructor(
|
||||
password = password,
|
||||
baseUrl = host,
|
||||
domainSuffix = domainSuffix,
|
||||
symbol = symbol
|
||||
defaultSymbol = symbol
|
||||
)
|
||||
}
|
||||
|
||||
@ -159,7 +166,7 @@ class LoginFormPresenter @Inject constructor(
|
||||
fun onSignInClick() {
|
||||
val loginData = getLoginData()
|
||||
|
||||
if (!validateCredentials(loginData.login, loginData.password, loginData.baseUrl)) return
|
||||
if (!validateCredentials(loginData)) return
|
||||
|
||||
resourceFlow {
|
||||
studentRepository.getUserSubjectsFromScrapper(
|
||||
@ -167,7 +174,7 @@ class LoginFormPresenter @Inject constructor(
|
||||
password = loginData.password,
|
||||
scrapperBaseUrl = loginData.baseUrl,
|
||||
domainSuffix = loginData.domainSuffix,
|
||||
symbol = loginData.symbol.orEmpty(),
|
||||
symbol = loginData.defaultSymbol,
|
||||
)
|
||||
}
|
||||
.logResourceStatus("login")
|
||||
@ -198,6 +205,9 @@ class LoginFormPresenter @Inject constructor(
|
||||
}
|
||||
.onResourceError {
|
||||
loginErrorHandler.dispatch(it)
|
||||
if (it is InvalidSymbolException) {
|
||||
loginErrorHandler.showDefaultMessage(it)
|
||||
}
|
||||
lastError = it
|
||||
view?.showContact(true)
|
||||
analytics.logEvent(
|
||||
@ -229,24 +239,29 @@ class LoginFormPresenter @Inject constructor(
|
||||
view?.onRecoverClick()
|
||||
}
|
||||
|
||||
private fun validateCredentials(login: String, password: String, host: String): Boolean {
|
||||
private fun validateCredentials(loginData: LoginData): Boolean {
|
||||
var isCorrect = true
|
||||
|
||||
if (login.isEmpty()) {
|
||||
if (loginData.login.isEmpty()) {
|
||||
view?.setErrorUsernameRequired()
|
||||
isCorrect = false
|
||||
} else {
|
||||
if ("@" in login && "login" in host) {
|
||||
if ("@" in loginData.login && "login" in loginData.baseUrl) {
|
||||
view?.setErrorLoginRequired()
|
||||
isCorrect = false
|
||||
}
|
||||
if ("@" !in login && "email" in host) {
|
||||
if ("@" !in loginData.login && "email" in loginData.baseUrl) {
|
||||
view?.setErrorEmailRequired()
|
||||
isCorrect = false
|
||||
}
|
||||
if ("@" in login && "||" !in login && "login" !in host && "email" !in host) {
|
||||
val emailHost = login.substringAfter("@")
|
||||
val emailDomain = URL(host).host
|
||||
|
||||
val isEmailLogin = "@" in loginData.login
|
||||
val isEmailWithLogin = "||" !in loginData.login
|
||||
val isLoginNotRequired = "login" !in loginData.baseUrl
|
||||
val isEmailNotRequired = "email" !in loginData.baseUrl
|
||||
if (isEmailLogin && isEmailWithLogin && isLoginNotRequired && isEmailNotRequired) {
|
||||
val emailHost = loginData.login.substringAfter("@")
|
||||
val emailDomain = URL(loginData.baseUrl).host
|
||||
if (!emailHost.equals(emailDomain, true)) {
|
||||
view?.setErrorEmailInvalid(domain = emailDomain)
|
||||
isCorrect = false
|
||||
@ -254,16 +269,21 @@ class LoginFormPresenter @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
if (password.isEmpty()) {
|
||||
if (loginData.password.isEmpty()) {
|
||||
view?.setErrorPassRequired(focus = isCorrect)
|
||||
isCorrect = false
|
||||
}
|
||||
|
||||
if (password.length < 6 && password.isNotEmpty()) {
|
||||
if (loginData.password.length < 6 && loginData.password.isNotEmpty()) {
|
||||
view?.setErrorPassInvalid(focus = isCorrect)
|
||||
isCorrect = false
|
||||
}
|
||||
|
||||
if (loginData.domainSuffix !in listOf("", "rc", "kurs")) {
|
||||
view?.setDomainSuffixInvalid()
|
||||
isCorrect = false
|
||||
}
|
||||
|
||||
return isCorrect
|
||||
}
|
||||
}
|
||||
|
@ -46,12 +46,16 @@ interface LoginFormView : BaseView {
|
||||
|
||||
fun setErrorEmailInvalid(domain: String)
|
||||
|
||||
fun setDomainSuffixInvalid()
|
||||
|
||||
fun clearUsernameError()
|
||||
|
||||
fun clearPassError()
|
||||
|
||||
fun clearHostError()
|
||||
|
||||
fun clearDomainSuffixError()
|
||||
|
||||
fun showSoftKeyboard()
|
||||
|
||||
fun hideSoftKeyboard()
|
||||
|
@ -38,7 +38,7 @@ class LoginRecoverPresenter @Inject constructor(
|
||||
|
||||
fun onHostSelected() {
|
||||
view?.run {
|
||||
if ("fakelog" in recoverHostValue) setDefaultCredentials("jan@fakelog.cf")
|
||||
if ("wulkanowy" in recoverHostValue) setDefaultCredentials("jan@fakelog.cf")
|
||||
clearUsernameError()
|
||||
updateFields()
|
||||
}
|
||||
@ -60,7 +60,7 @@ class LoginRecoverPresenter @Inject constructor(
|
||||
resourceFlow {
|
||||
recoverRepository.getReCaptchaSiteKey(
|
||||
host,
|
||||
symbol.ifBlank { "Default" })
|
||||
symbol.ifBlank { "default" })
|
||||
}.onEach {
|
||||
when (it) {
|
||||
is Resource.Loading -> view?.run {
|
||||
@ -103,7 +103,7 @@ class LoginRecoverPresenter @Inject constructor(
|
||||
fun onReCaptchaVerified(reCaptchaResponse: String) {
|
||||
val username = view?.recoverNameValue.orEmpty()
|
||||
val host = view?.recoverHostValue.orEmpty()
|
||||
val symbol = view?.formHostSymbol.ifNullOrBlank { "Default" }
|
||||
val symbol = view?.formHostSymbol.ifNullOrBlank { "default" }
|
||||
|
||||
resourceFlow {
|
||||
recoverRepository.sendRecoverRequest(
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user