Compare commits
111 Commits
Author | SHA1 | Date | |
---|---|---|---|
a69361708e | |||
567d868f76 | |||
12030efee2 | |||
3cbe98d7b8 | |||
503a97bc4c | |||
04c382643d | |||
fc140ad9c1 | |||
ae6a35121b | |||
78a2cc89e9 | |||
dbfe5c8918 | |||
c697ca7ad1 | |||
1b00f4e518 | |||
fb77bf882f | |||
466ebbef3a | |||
458a4c8164 | |||
0363c0854f | |||
558addd097 | |||
233ddc955b | |||
065c711f91 | |||
49655c11c9 | |||
38fd4eda22 | |||
e8f9c57c34 | |||
b71630246a | |||
fd2eac1f08 | |||
1545ff65d3 | |||
ff32c82851 | |||
d531a94594 | |||
71ab9586ac | |||
e1a19be06c | |||
6f2168d641 | |||
cde2121b60 | |||
a0bc37e826 | |||
4d67de8e5f | |||
f983a23b1a | |||
6a1851da13 | |||
ad5381ce34 | |||
dbc7587741 | |||
bc3aa7b8dc | |||
6bf6a9da11 | |||
ab175bdd9a | |||
8dbbea2138 | |||
f6226e6b53 | |||
43d13db07c | |||
82210c37e3 | |||
2816d7217a | |||
2fa868173b | |||
622c75bb42 | |||
2121125283 | |||
c72a117e34 | |||
b5cc32d59f | |||
d943d03266 | |||
6eca8c42f5 | |||
af989ba9f6 | |||
4a65a5b192 | |||
bbbafdfe70 | |||
860095e862 | |||
ff9be43291 | |||
a487378daf | |||
895f5cbb76 | |||
8b9b1460ab | |||
7edd3df074 | |||
16c51f7b07 | |||
7a3a97447f | |||
b500d8e204 | |||
c34a369286 | |||
b9f3ab2e56 | |||
6b59973624 | |||
d18485293d | |||
a82e11d694 | |||
4dc5fc65ac | |||
7463cf6253 | |||
d799ec7ac9 | |||
254719f22f | |||
596e8df4fc | |||
e1e276e1ea | |||
8cdd4311a9 | |||
b7f7b16aef | |||
f13ce6e2b4 | |||
8c10606b61 | |||
7fda4276d6 | |||
7993366bfc | |||
2e71c50894 | |||
b3faac01a5 | |||
3881678208 | |||
76d038eefa | |||
3a55c3c760 | |||
a0818de7d1 | |||
b280316b07 | |||
0554aa91fd | |||
5a77d1e940 | |||
c9a42a6cf6 | |||
27eb0588d7 | |||
34d34a050a | |||
961bc24f27 | |||
8a90b61b97 | |||
6a8f6f9496 | |||
afb5ae741c | |||
95e41b5570 | |||
eb6fdd900e | |||
88def5eff8 | |||
0e99c81eb8 | |||
38c00ddab5 | |||
c72cc39920 | |||
4ef9fb1f28 | |||
5dd5697f65 | |||
a7c2009e49 | |||
a71ef4a4b2 | |||
30413086fc | |||
98ddf97855 | |||
8f5a210ec7 | |||
ce09b07cfd |
@ -162,7 +162,7 @@ jobs:
|
||||
openssl aes-256-cbc -d -in ./app/upload-key-encrypted.jks -k $ENCRYPT_KEY >> ./app/upload-key.jks
|
||||
- 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
|
||||
|
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:
|
||||
|
11
.gitignore
vendored
11
.gitignore
vendored
@ -117,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
|
||||
|
@ -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 151
|
||||
versionName "2.5.2"
|
||||
versionCode 164
|
||||
versionName "2.6.4"
|
||||
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"'
|
||||
}
|
||||
@ -164,8 +160,8 @@ play {
|
||||
defaultToAppBundles = false
|
||||
track = 'production'
|
||||
releaseStatus = ReleaseStatus.IN_PROGRESS
|
||||
userFraction = 0.50d
|
||||
updatePriority = 3
|
||||
userFraction = 0.99d
|
||||
updatePriority = 4
|
||||
enabled.set(false)
|
||||
}
|
||||
|
||||
@ -195,23 +191,23 @@ ext {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'io.github.wulkanowy:sdk:2.5.2'
|
||||
implementation 'io.github.wulkanowy:sdk:2.6.3'
|
||||
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
|
||||
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines"
|
||||
|
||||
implementation 'androidx.core:core-ktx:1.12.0'
|
||||
implementation 'androidx.core:core-ktx:1.13.1'
|
||||
implementation 'androidx.core:core-splashscreen:1.0.1'
|
||||
implementation "androidx.activity:activity-ktx:1.8.2"
|
||||
implementation "androidx.activity:activity-ktx:1.9.0"
|
||||
implementation "androidx.appcompat:appcompat:1.6.1"
|
||||
implementation "androidx.fragment:fragment-ktx:1.6.2"
|
||||
implementation "androidx.fragment:fragment-ktx:1.7.0"
|
||||
implementation "androidx.annotation:annotation:1.7.1"
|
||||
|
||||
implementation "androidx.preference:preference-ktx:1.2.1"
|
||||
implementation "androidx.recyclerview:recyclerview:1.3.2"
|
||||
implementation "androidx.viewpager2:viewpager2:1.1.0-beta02"
|
||||
implementation "androidx.viewpager2:viewpager2:1.1.0-rc01"
|
||||
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
|
||||
implementation "androidx.constraintlayout:constraintlayout:2.1.4"
|
||||
implementation "androidx.coordinatorlayout:coordinatorlayout:1.2.0"
|
||||
@ -237,7 +233,7 @@ dependencies {
|
||||
implementation 'com.github.ncapdevi:FragNav:3.3.0'
|
||||
implementation "com.github.YarikSOffice:lingver:1.3.0"
|
||||
|
||||
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
|
||||
implementation 'com.squareup.retrofit2:retrofit:2.11.0'
|
||||
implementation "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0"
|
||||
implementation "com.squareup.okhttp3:logging-interceptor:4.12.0"
|
||||
implementation "com.squareup.okhttp3:okhttp-urlconnection:4.12.0"
|
||||
@ -250,9 +246,9 @@ dependencies {
|
||||
implementation "io.github.wulkanowy:AppKillerManager:3.0.1"
|
||||
implementation 'me.xdrop:fuzzywuzzy:1.4.0'
|
||||
implementation 'com.fredporciuncula:flow-preferences:1.9.1'
|
||||
implementation 'org.apache.commons:commons-text:1.11.0'
|
||||
implementation 'org.apache.commons:commons-text:1.12.0'
|
||||
|
||||
playImplementation platform('com.google.firebase:firebase-bom:32.7.3')
|
||||
playImplementation platform('com.google.firebase:firebase-bom:33.0.0')
|
||||
playImplementation 'com.google.firebase:firebase-analytics'
|
||||
playImplementation 'com.google.firebase:firebase-messaging'
|
||||
playImplementation 'com.google.firebase:firebase-crashlytics:'
|
||||
@ -278,7 +274,7 @@ dependencies {
|
||||
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines"
|
||||
testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
|
||||
|
||||
testImplementation 'org.robolectric:robolectric:4.11.1'
|
||||
testImplementation 'org.robolectric:robolectric:4.12.1'
|
||||
testImplementation "androidx.test:runner:1.5.2"
|
||||
testImplementation "androidx.test.ext:junit:1.1.5"
|
||||
testImplementation "androidx.test:core:1.5.0"
|
||||
|
@ -36,6 +36,37 @@
|
||||
"status": 2
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:1091101852179:android:b558a25f65d088b1",
|
||||
"android_client_info": {
|
||||
"package_name": "io.github.wulkanowy"
|
||||
}
|
||||
},
|
||||
"oauth_client": [
|
||||
{
|
||||
"client_id": "",
|
||||
"client_type": 3
|
||||
}
|
||||
],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": ""
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"analytics_service": {
|
||||
"status": 1
|
||||
},
|
||||
"appinvite_service": {
|
||||
"status": 1,
|
||||
"other_platform_oauth_client": []
|
||||
},
|
||||
"ads_service": {
|
||||
"status": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"configuration_version": "1"
|
@ -1,7 +1,8 @@
|
||||
#!/bin/bash -
|
||||
|
||||
content=$(cat < "app/src/main/play/release-notes/pl-PL/default.txt") || exit
|
||||
if [[ "${#content}" -gt 500 ]]; then
|
||||
content2=echo "$content" | dos2unix
|
||||
if [[ "${#content2}" -gt 500 ]]; then
|
||||
echo >&2 "Release notes content has reached the limit of 500 characters"
|
||||
exit 1
|
||||
fi
|
||||
|
2547
app/schemas/io.github.wulkanowy.data.db.AppDatabase/63.json
Normal file
2547
app/schemas/io.github.wulkanowy.data.db.AppDatabase/63.json
Normal file
File diff suppressed because it is too large
Load Diff
2559
app/schemas/io.github.wulkanowy.data.db.AppDatabase/64.json
Normal file
2559
app/schemas/io.github.wulkanowy.data.db.AppDatabase/64.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,92 +0,0 @@
|
||||
{
|
||||
"agcgw": {
|
||||
"backurl": "connect-dre.hispace.hicloud.com",
|
||||
"url": "connect-dre.dbankcloud.cn",
|
||||
"websocketbackurl": "connect-ws-dre.hispace.dbankcloud.com",
|
||||
"websocketurl": "connect-ws-dre.hispace.dbankcloud.cn"
|
||||
},
|
||||
"agcgw_all": {
|
||||
"CN": "connect-drcn.dbankcloud.cn",
|
||||
"CN_back": "connect-drcn.hispace.hicloud.com",
|
||||
"DE": "connect-dre.dbankcloud.cn",
|
||||
"DE_back": "connect-dre.hispace.hicloud.com",
|
||||
"RU": "connect-drru.hispace.dbankcloud.ru",
|
||||
"RU_back": "connect-drru.hispace.dbankcloud.cn",
|
||||
"SG": "connect-dra.dbankcloud.cn",
|
||||
"SG_back": "connect-dra.hispace.hicloud.com"
|
||||
},
|
||||
"websocketgw_all": {
|
||||
"CN": "connect-ws-drcn.hispace.dbankcloud.cn",
|
||||
"CN_back": "connect-ws-drcn.hispace.dbankcloud.com",
|
||||
"DE": "connect-ws-dre.hispace.dbankcloud.cn",
|
||||
"DE_back": "connect-ws-dre.hispace.dbankcloud.com",
|
||||
"RU": "connect-ws-drru.hispace.dbankcloud.ru",
|
||||
"RU_back": "connect-ws-drru.hispace.dbankcloud.cn",
|
||||
"SG": "connect-ws-dra.hispace.dbankcloud.cn",
|
||||
"SG_back": "connect-ws-dra.hispace.dbankcloud.com"
|
||||
},
|
||||
"client": {
|
||||
"cp_id": "890048000024105546",
|
||||
"product_id": "736430079244736562",
|
||||
"client_id": "514530959291319360",
|
||||
"client_secret": "C42522DBF17D3D4BBE9D9C1783A54484B7E6844B388B7A67502D36A633A4186B",
|
||||
"project_id": "736430079244736562",
|
||||
"app_id": "106552551",
|
||||
"api_key": "CgB6e3x9BUNiq+r8ebCHNojjjYsMT4pJSjjNDOkm9owtBb6rVI6LjnASoZBRxbjjhObcrV5gANo99fI/eKZDTbWS",
|
||||
"package_name": "io.github.wulkanowy.dev"
|
||||
},
|
||||
"oauth_client": {
|
||||
"client_id": "106552551",
|
||||
"client_type": 1
|
||||
},
|
||||
"app_info": {
|
||||
"app_id": "106552551",
|
||||
"package_name": "io.github.wulkanowy.dev"
|
||||
},
|
||||
"service": {
|
||||
"analytics": {
|
||||
"collector_url": "datacollector-dre.dt.hicloud.com,datacollector-dre.dt.dbankcloud.cn",
|
||||
"collector_url_ru": "datacollector-drru.dt.dbankcloud.ru,datacollector-drru.dt.hicloud.com",
|
||||
"collector_url_sg": "datacollector-dra.dt.hicloud.com,datacollector-dra.dt.dbankcloud.cn",
|
||||
"collector_url_de": "datacollector-dre.dt.hicloud.com,datacollector-dre.dt.dbankcloud.cn",
|
||||
"collector_url_cn": "datacollector-drcn.dt.hicloud.com,datacollector-drcn.dt.dbankcloud.cn",
|
||||
"resource_id": "p1",
|
||||
"channel_id": ""
|
||||
},
|
||||
"search":{
|
||||
"url":"https://search-dre.cloud.huawei.com"
|
||||
},
|
||||
"cloudstorage": {
|
||||
"storage_url_sg_back": "https://agc-storage-dra.cloud.huawei.asia",
|
||||
"storage_url_ru_back": "https://agc-storage-drru.cloud.huawei.ru",
|
||||
"storage_url_ru": "https://agc-storage-drru.cloud.huawei.ru",
|
||||
"storage_url_de_back": "https://agc-storage-dre.cloud.huawei.eu",
|
||||
"storage_url_de": "https://ops-dre.agcstorage.link",
|
||||
"storage_url": "https://agc-storage-drcn.platform.dbankcloud.cn",
|
||||
"storage_url_sg": "https://ops-dra.agcstorage.link",
|
||||
"storage_url_cn_back": "https://agc-storage-drcn.cloud.huawei.com.cn",
|
||||
"storage_url_cn": "https://agc-storage-drcn.platform.dbankcloud.cn"
|
||||
},
|
||||
"ml": {
|
||||
"mlservice_url": "ml-api-dre.ai.dbankcloud.com,ml-api-dre.ai.dbankcloud.cn"
|
||||
}
|
||||
},
|
||||
"region": "DE",
|
||||
"configuration_version": "3.0",
|
||||
"appInfos": [
|
||||
{
|
||||
"package_name": "io.github.wulkanowy.dev",
|
||||
"client": {
|
||||
"app_id": "106552551"
|
||||
},
|
||||
"app_info": {
|
||||
"package_name": "io.github.wulkanowy.dev",
|
||||
"app_id": "106552551"
|
||||
},
|
||||
"oauth_client": {
|
||||
"client_type": 1,
|
||||
"client_id": "106552551"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@ -51,7 +51,7 @@
|
||||
android:exported="true"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/WulkanowyTheme.SplashScreen"
|
||||
tools:ignore="LockedOrientationActivity">
|
||||
tools:ignore="DiscouragedApi,LockedOrientationActivity">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
@ -155,33 +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" />
|
||||
|
@ -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,23 +32,6 @@ import javax.inject.Singleton
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal class DataModule {
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideSdk(
|
||||
chuckerInterceptor: ChuckerInterceptor,
|
||||
remoteConfig: RemoteConfigHelper,
|
||||
webkitCookieManagerProxy: WebkitCookieManagerProxy,
|
||||
) = 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(
|
||||
|
@ -1,11 +1,17 @@
|
||||
package io.github.wulkanowy.data
|
||||
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.filterNot
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
@ -14,16 +20,39 @@ import kotlinx.coroutines.flow.takeWhile
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import timber.log.Timber
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
sealed class Resource<T> {
|
||||
|
||||
open class Loading<T> : Resource<T>()
|
||||
sealed interface Resource<out T> {
|
||||
/**
|
||||
* The initial value of a resource flow. Indicates no data that is currently available to be shown,
|
||||
* however with the expectation that the state will transition to another one soon.
|
||||
*/
|
||||
open class Loading<T> : Resource<T>
|
||||
|
||||
/**
|
||||
* A semi-loading state with some data available to be displayed (usually cached data loaded from
|
||||
* the database). Still not the target state and it's expected to transition into another one soon.
|
||||
*/
|
||||
data class Intermediate<T>(val data: T) : Loading<T>()
|
||||
|
||||
data class Success<T>(val data: T) : Resource<T>()
|
||||
/**
|
||||
* The happy-path target state. Data can either be:
|
||||
* - loaded from the database - while it may seem like this case is already handled by the
|
||||
* Intermediate state, the difference here is semantic. Cached data is returned as Intermediate
|
||||
* when there's a API request in progress (or soon expected to be), however when there is no
|
||||
* intention of immediately querying the API, the cached data is returned as a Success.
|
||||
* - fetched from the API.
|
||||
*/
|
||||
data class Success<T>(val data: T) : Resource<T>
|
||||
|
||||
data class Error<T>(val error: Throwable) : Resource<T>()
|
||||
/**
|
||||
* Something bad happened and we were unable to get the requested data. This can be caused by
|
||||
* a database error, a network error, or really just any other error. Upon receiving this state
|
||||
* the UI can either: display a full screen error, or, when it has received any data previously,
|
||||
* display a snack bar informing of the problem.
|
||||
*/
|
||||
data class Error<T>(val error: Throwable) : Resource<T>
|
||||
}
|
||||
|
||||
val <T> Resource<T>.dataOrNull: T?
|
||||
@ -64,6 +93,22 @@ fun <T, U> Resource<T>.mapData(block: (T) -> U) = when (this) {
|
||||
is Resource.Error -> Resource.Error(this.error)
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects another flow into this flow's resource data.
|
||||
*/
|
||||
inline fun <T1, T2, R> Flow<Resource<T1>>.combineWithResourceData(
|
||||
flow: Flow<T2>,
|
||||
crossinline block: suspend (T1, T2) -> R
|
||||
): Flow<Resource<R>> =
|
||||
combine(flow) { resource, inject ->
|
||||
when (resource) {
|
||||
is Resource.Success -> Resource.Success(block(resource.data, inject))
|
||||
is Resource.Intermediate -> Resource.Intermediate(block(resource.data, inject))
|
||||
is Resource.Loading -> Resource.Loading()
|
||||
is Resource.Error -> Resource.Error(resource.error)
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> Flow<Resource<T>>.logResourceStatus(name: String, showData: Boolean = false) = onEach {
|
||||
val description = when (it) {
|
||||
is Resource.Intermediate -> "intermediate data received" + if (showData) " (data: `${it.data}`)" else ""
|
||||
@ -74,8 +119,29 @@ fun <T> Flow<Resource<T>>.logResourceStatus(name: String, showData: Boolean = fa
|
||||
Timber.i("$name: $description")
|
||||
}
|
||||
|
||||
fun <T, U> Flow<Resource<T>>.mapResourceData(block: (T) -> U) = map {
|
||||
it.mapData(block)
|
||||
inline fun <T, U> Flow<Resource<T>>.mapResourceData(crossinline block: suspend (T) -> U) = map {
|
||||
when (it) {
|
||||
is Resource.Success -> Resource.Success(block(it.data))
|
||||
is Resource.Intermediate -> Resource.Intermediate(block(it.data))
|
||||
is Resource.Loading -> Resource.Loading()
|
||||
is Resource.Error -> Resource.Error(it.error)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun <T, U> Flow<Resource<T>>.flatMapResourceData(
|
||||
inheritIntermediate: Boolean = true, block: suspend (T) -> Flow<Resource<U>>
|
||||
) = flatMapLatest {
|
||||
when (it) {
|
||||
is Resource.Success -> block(it.data)
|
||||
is Resource.Intermediate -> block(it.data).map { newRes ->
|
||||
if (inheritIntermediate && newRes is Resource.Success) Resource.Intermediate(newRes.data)
|
||||
else newRes
|
||||
}
|
||||
|
||||
is Resource.Loading -> flowOf(Resource.Loading())
|
||||
is Resource.Error -> flowOf(Resource.Error(it.error))
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> Flow<Resource<T>>.onResourceData(block: suspend (T) -> Unit) = onEach {
|
||||
@ -105,13 +171,13 @@ fun <T> Flow<Resource<T>>.onResourceSuccess(block: suspend (T) -> Unit) = onEach
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> Flow<Resource<T>>.onResourceError(block: (Throwable) -> Unit) = onEach {
|
||||
fun <T> Flow<Resource<T>>.onResourceError(block: suspend (Throwable) -> Unit) = onEach {
|
||||
if (it is Resource.Error) {
|
||||
block(it.error)
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> Flow<Resource<T>>.onResourceNotLoading(block: () -> Unit) = onEach {
|
||||
fun <T> Flow<Resource<T>>.onResourceNotLoading(block: suspend () -> Unit) = onEach {
|
||||
if (it !is Resource.Loading) {
|
||||
block()
|
||||
}
|
||||
@ -121,70 +187,99 @@ suspend fun <T> Flow<Resource<T>>.toFirstResult() = filter { it !is Resource.Loa
|
||||
|
||||
suspend fun <T> Flow<Resource<T>>.waitForResult() = takeWhile { it is Resource.Loading }.collect()
|
||||
|
||||
inline fun <ResultType, RequestType> networkBoundResource(
|
||||
mutex: Mutex = Mutex(),
|
||||
showSavedOnLoading: Boolean = true,
|
||||
crossinline isResultEmpty: (ResultType) -> Boolean,
|
||||
crossinline query: () -> Flow<ResultType>,
|
||||
crossinline fetch: suspend (ResultType) -> RequestType,
|
||||
crossinline saveFetchResult: suspend (old: ResultType, new: RequestType) -> Unit,
|
||||
crossinline onFetchFailed: (Throwable) -> Unit = { },
|
||||
crossinline shouldFetch: (ResultType) -> Boolean = { true },
|
||||
crossinline filterResult: (ResultType) -> ResultType = { it }
|
||||
) = flow {
|
||||
emit(Resource.Loading())
|
||||
// Can cause excessive amounts of `Resource.Intermediate` to be emitted. Unless that is desired,
|
||||
// use `debounceIntermediates` to alleviate this behavior.
|
||||
inline fun <reified T> combineResourceFlows(flows: Iterable<Flow<Resource<T>>>): Flow<Resource<List<T>>> =
|
||||
combine(flows) { items ->
|
||||
var isIntermediate = false
|
||||
val data = mutableListOf<T>()
|
||||
for (item in items) {
|
||||
when (item) {
|
||||
is Resource.Success -> data.add(item.data)
|
||||
is Resource.Intermediate -> {
|
||||
isIntermediate = true
|
||||
data.add(item.data)
|
||||
}
|
||||
|
||||
val data = query().first()
|
||||
emitAll(if (shouldFetch(data)) {
|
||||
val filteredResult = filterResult(data)
|
||||
|
||||
if (showSavedOnLoading && !isResultEmpty(filteredResult)) {
|
||||
emit(Resource.Intermediate(filteredResult))
|
||||
is Resource.Loading -> return@combine Resource.Loading()
|
||||
is Resource.Error -> continue
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
val newData = fetch(data)
|
||||
mutex.withLock { saveFetchResult(query().first(), newData) }
|
||||
query().map { Resource.Success(filterResult(it)) }
|
||||
} catch (throwable: Throwable) {
|
||||
onFetchFailed(throwable)
|
||||
flowOf(Resource.Error(throwable))
|
||||
if (data.isEmpty()) {
|
||||
// All items have to be errors for this to happen, so just return the first one.
|
||||
// mapData is functionally useless and exists only to satisfy the type checker
|
||||
items.first().mapData { listOf(it) }
|
||||
} else if (isIntermediate) {
|
||||
Resource.Intermediate(data)
|
||||
} else {
|
||||
Resource.Success(data)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
fun <T> Flow<Resource<T>>.debounceIntermediates(timeout: Duration = 5.seconds) = flow {
|
||||
var wasIntermediate = false
|
||||
|
||||
emitAll(this@debounceIntermediates.debounce {
|
||||
if (it is Resource.Intermediate) {
|
||||
if (!wasIntermediate) {
|
||||
wasIntermediate = true
|
||||
Duration.ZERO
|
||||
} else {
|
||||
timeout
|
||||
}
|
||||
} else {
|
||||
wasIntermediate = false
|
||||
Duration.ZERO
|
||||
}
|
||||
} else {
|
||||
query().map { Resource.Success(filterResult(it)) }
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
inline fun <OutputType, ApiType> networkBoundResource(
|
||||
mutex: Mutex = Mutex(),
|
||||
crossinline isResultEmpty: (OutputType) -> Boolean,
|
||||
crossinline query: () -> Flow<OutputType>,
|
||||
crossinline fetch: suspend () -> ApiType,
|
||||
crossinline saveFetchResult: suspend (old: OutputType, new: ApiType) -> Unit,
|
||||
crossinline shouldFetch: (OutputType) -> Boolean = { true },
|
||||
crossinline filterResult: (OutputType) -> OutputType = { it }
|
||||
) = networkBoundResource(
|
||||
mutex = mutex,
|
||||
isResultEmpty = isResultEmpty,
|
||||
query = query,
|
||||
fetch = fetch,
|
||||
saveFetchResult = saveFetchResult,
|
||||
shouldFetch = shouldFetch,
|
||||
mapResult = filterResult
|
||||
)
|
||||
|
||||
@JvmName("networkBoundResourceWithMap")
|
||||
inline fun <ResultType, RequestType, T> networkBoundResource(
|
||||
inline fun <DatabaseType, ApiType, OutputType> networkBoundResource(
|
||||
mutex: Mutex = Mutex(),
|
||||
showSavedOnLoading: Boolean = true,
|
||||
crossinline isResultEmpty: (T) -> Boolean,
|
||||
crossinline query: () -> Flow<ResultType>,
|
||||
crossinline fetch: suspend (ResultType) -> RequestType,
|
||||
crossinline saveFetchResult: suspend (old: ResultType, new: RequestType) -> Unit,
|
||||
crossinline onFetchFailed: (Throwable) -> Unit = { },
|
||||
crossinline shouldFetch: (ResultType) -> Boolean = { true },
|
||||
crossinline mapResult: (ResultType) -> T,
|
||||
crossinline isResultEmpty: (OutputType) -> Boolean,
|
||||
crossinline query: () -> Flow<DatabaseType>,
|
||||
crossinline fetch: suspend () -> ApiType,
|
||||
crossinline saveFetchResult: suspend (old: DatabaseType, new: ApiType) -> Unit,
|
||||
crossinline shouldFetch: (DatabaseType) -> Boolean = { true },
|
||||
crossinline mapResult: (DatabaseType) -> OutputType,
|
||||
) = flow {
|
||||
emit(Resource.Loading())
|
||||
|
||||
val data = query().first()
|
||||
emitAll(if (shouldFetch(data)) {
|
||||
val mappedResult = mapResult(data)
|
||||
if (shouldFetch(data)) {
|
||||
emit(Resource.Intermediate(data))
|
||||
|
||||
if (showSavedOnLoading && !isResultEmpty(mappedResult)) {
|
||||
emit(Resource.Intermediate(mappedResult))
|
||||
}
|
||||
try {
|
||||
val newData = fetch(data)
|
||||
val newData = fetch()
|
||||
mutex.withLock { saveFetchResult(query().first(), newData) }
|
||||
query().map { Resource.Success(mapResult(it)) }
|
||||
} catch (throwable: Throwable) {
|
||||
onFetchFailed(throwable)
|
||||
flowOf(Resource.Error(throwable))
|
||||
emit(Resource.Error(throwable))
|
||||
return@flow
|
||||
}
|
||||
} else {
|
||||
query().map { Resource.Success(mapResult(it)) }
|
||||
})
|
||||
}
|
||||
|
||||
emitAll(query().map { Resource.Success(it) })
|
||||
}
|
||||
.mapResourceData { mapResult(it) }
|
||||
.filterNot { it is Resource.Intermediate && isResultEmpty(it.data) }
|
||||
|
@ -0,0 +1,125 @@
|
||||
package io.github.wulkanowy.data
|
||||
|
||||
import com.chuckerteam.chucker.api.ChuckerInterceptor
|
||||
import io.github.wulkanowy.data.db.dao.StudentDao
|
||||
import io.github.wulkanowy.data.db.entities.Semester
|
||||
import io.github.wulkanowy.data.db.entities.Student
|
||||
import io.github.wulkanowy.data.db.entities.StudentIsEduOne
|
||||
import io.github.wulkanowy.sdk.Sdk
|
||||
import io.github.wulkanowy.utils.RemoteConfigHelper
|
||||
import io.github.wulkanowy.utils.WebkitCookieManagerProxy
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
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 studentDb: StudentDao,
|
||||
) {
|
||||
|
||||
private val eduOneMutex = Mutex()
|
||||
private val migrationFailedStudentIds = mutableSetOf<Long>()
|
||||
|
||||
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
|
||||
|
||||
suspend fun create(student: Student, semester: Semester? = null): Sdk {
|
||||
val overrideIsEduOne = checkEduOneAndMigrateIfNecessary(student)
|
||||
return buildSdk(student, semester, overrideIsEduOne)
|
||||
}
|
||||
|
||||
private fun buildSdk(student: Student, semester: Semester?, isStudentEduOne: Boolean): Sdk {
|
||||
return create().apply {
|
||||
email = student.email
|
||||
password = student.password
|
||||
symbol = student.symbol
|
||||
schoolSymbol = student.schoolSymbol
|
||||
studentId = student.studentId
|
||||
classId = student.classId
|
||||
emptyCookieJarInterceptor = true
|
||||
isEduOne = isStudentEduOne
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun checkEduOneAndMigrateIfNecessary(student: Student): Boolean {
|
||||
if (student.isEduOne != null) return student.isEduOne
|
||||
|
||||
if (student.id in migrationFailedStudentIds) {
|
||||
Timber.i("Migration eduOne: skipping because of previous failure")
|
||||
return false
|
||||
}
|
||||
|
||||
eduOneMutex.withLock {
|
||||
if (student.id in migrationFailedStudentIds) {
|
||||
Timber.i("Migration eduOne: skipping because of previous failure")
|
||||
return false
|
||||
}
|
||||
|
||||
val studentFromDatabase = studentDb.loadById(student.id)
|
||||
if (studentFromDatabase?.isEduOne != null) {
|
||||
Timber.i("Migration eduOne: already done")
|
||||
return studentFromDatabase.isEduOne
|
||||
}
|
||||
|
||||
Timber.i("Migration eduOne: flag missing. Running migration...")
|
||||
val initializedSdk = buildSdk(
|
||||
student = student,
|
||||
semester = null,
|
||||
isStudentEduOne = false, // doesn't matter
|
||||
)
|
||||
val newCurrentStudent = runCatching { initializedSdk.getCurrentStudent() }
|
||||
.onFailure { Timber.e(it, "Migration eduOne: can't get current student") }
|
||||
.getOrNull()
|
||||
|
||||
if (newCurrentStudent == null) {
|
||||
Timber.i("Migration eduOne: failed, so skipping")
|
||||
migrationFailedStudentIds.add(student.id)
|
||||
return false
|
||||
}
|
||||
|
||||
Timber.i("Migration eduOne: success. New isEduOne flag: ${newCurrentStudent.isEduOne}")
|
||||
|
||||
val studentIsEduOne = StudentIsEduOne(
|
||||
id = student.id,
|
||||
isEduOne = newCurrentStudent.isEduOne
|
||||
)
|
||||
studentDb.update(studentIsEduOne)
|
||||
return newCurrentStudent.isEduOne
|
||||
}
|
||||
}
|
||||
}
|
@ -120,6 +120,7 @@ import io.github.wulkanowy.data.db.migrations.Migration55
|
||||
import io.github.wulkanowy.data.db.migrations.Migration57
|
||||
import io.github.wulkanowy.data.db.migrations.Migration58
|
||||
import io.github.wulkanowy.data.db.migrations.Migration6
|
||||
import io.github.wulkanowy.data.db.migrations.Migration63
|
||||
import io.github.wulkanowy.data.db.migrations.Migration7
|
||||
import io.github.wulkanowy.data.db.migrations.Migration8
|
||||
import io.github.wulkanowy.data.db.migrations.Migration9
|
||||
@ -175,6 +176,8 @@ import javax.inject.Singleton
|
||||
AutoMigration(from = 59, to = 60),
|
||||
AutoMigration(from = 60, to = 61),
|
||||
AutoMigration(from = 61, to = 62),
|
||||
AutoMigration(from = 62, to = 63, spec = Migration63::class),
|
||||
AutoMigration(from = 63, to = 64),
|
||||
],
|
||||
version = AppDatabase.VERSION_SCHEMA,
|
||||
exportSchema = true
|
||||
@ -183,7 +186,7 @@ import javax.inject.Singleton
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
|
||||
companion object {
|
||||
const val VERSION_SCHEMA = 62
|
||||
const val VERSION_SCHEMA = 64
|
||||
|
||||
fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf(
|
||||
Migration2(),
|
||||
|
@ -8,6 +8,6 @@ import kotlinx.coroutines.flow.Flow
|
||||
@Dao
|
||||
interface MobileDeviceDao : BaseDao<MobileDevice> {
|
||||
|
||||
@Query("SELECT * FROM MobileDevices WHERE user_login_id = :userLoginId ORDER BY date DESC")
|
||||
fun loadAll(userLoginId: Int): Flow<List<MobileDevice>>
|
||||
@Query("SELECT * FROM MobileDevices WHERE user_login_id = :studentId ORDER BY date DESC")
|
||||
fun loadAll(studentId: Int): Flow<List<MobileDevice>>
|
||||
}
|
||||
|
@ -10,6 +10,6 @@ import javax.inject.Singleton
|
||||
@Singleton
|
||||
interface SchoolAnnouncementDao : BaseDao<SchoolAnnouncement> {
|
||||
|
||||
@Query("SELECT * FROM SchoolAnnouncements WHERE user_login_id = :userLoginId ORDER BY date DESC")
|
||||
fun loadAll(userLoginId: Int): Flow<List<SchoolAnnouncement>>
|
||||
@Query("SELECT * FROM SchoolAnnouncements WHERE user_login_id = :studentId ORDER BY date DESC")
|
||||
fun loadAll(studentId: Int): Flow<List<SchoolAnnouncement>>
|
||||
}
|
||||
|
@ -14,6 +14,6 @@ interface SemesterDao : BaseDao<Semester> {
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
suspend fun insertSemesters(items: List<Semester>): List<Long>
|
||||
|
||||
@Query("SELECT * FROM Semesters WHERE student_id = :studentId AND class_id = :classId")
|
||||
@Query("SELECT * FROM Semesters WHERE (student_id = :studentId AND class_id = :classId) OR (student_id = :studentId AND class_id = 0)")
|
||||
suspend fun loadAll(studentId: Int, classId: Int): List<Semester>
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import androidx.room.Update
|
||||
import io.github.wulkanowy.data.db.entities.Semester
|
||||
import io.github.wulkanowy.data.db.entities.Student
|
||||
import io.github.wulkanowy.data.db.entities.StudentIsAuthorized
|
||||
import io.github.wulkanowy.data.db.entities.StudentIsEduOne
|
||||
import io.github.wulkanowy.data.db.entities.StudentName
|
||||
import io.github.wulkanowy.data.db.entities.StudentNickAndAvatar
|
||||
import javax.inject.Singleton
|
||||
@ -27,6 +28,9 @@ abstract class StudentDao {
|
||||
@Update(entity = Student::class)
|
||||
abstract suspend fun update(studentIsAuthorized: StudentIsAuthorized)
|
||||
|
||||
@Update(entity = Student::class)
|
||||
abstract suspend fun update(studentIsEduOne: StudentIsEduOne)
|
||||
|
||||
@Update(entity = Student::class)
|
||||
abstract suspend fun update(studentNickAndAvatar: StudentNickAndAvatar)
|
||||
|
||||
@ -43,11 +47,11 @@ abstract class StudentDao {
|
||||
abstract suspend fun loadAll(): List<Student>
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM Students JOIN Semesters ON Students.student_id = Semesters.student_id AND Students.class_id = Semesters.class_id")
|
||||
@Query("SELECT * FROM Students JOIN Semesters ON (Students.student_id = Semesters.student_id AND Students.class_id = Semesters.class_id) OR (Students.student_id = Semesters.student_id AND Semesters.class_id = 0)")
|
||||
abstract suspend fun loadStudentsWithSemesters(): Map<Student, List<Semester>>
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM Students JOIN Semesters ON Students.student_id = Semesters.student_id AND Students.class_id = Semesters.class_id WHERE Students.id = :id")
|
||||
@Query("SELECT * FROM Students JOIN Semesters ON (Students.student_id = Semesters.student_id AND Students.class_id = Semesters.class_id) OR (Students.student_id = Semesters.student_id AND Semesters.class_id = 0) WHERE Students.id = :id")
|
||||
abstract suspend fun loadStudentWithSemestersById(id: Long): Map<Student, List<Semester>>
|
||||
|
||||
@Query("UPDATE Students SET is_current = 1 WHERE id = :id")
|
||||
|
@ -4,6 +4,8 @@ import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import io.github.wulkanowy.data.enums.MessageType
|
||||
import io.github.wulkanowy.data.serializers.SafeMessageTypeEnumListSerializer
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
@ -34,6 +36,8 @@ data class AdminMessage(
|
||||
|
||||
val priority: String,
|
||||
|
||||
@SerialName("messageTypes")
|
||||
@Serializable(with = SafeMessageTypeEnumListSerializer::class)
|
||||
@ColumnInfo(name = "types", defaultValue = "[]")
|
||||
val types: List<MessageType> = emptyList(),
|
||||
|
||||
|
@ -33,7 +33,13 @@ data class GradeSummary(
|
||||
@ColumnInfo(name = "points_sum")
|
||||
val pointsSum: String,
|
||||
|
||||
val average: Double
|
||||
@ColumnInfo(name = "points_sum_all_year")
|
||||
val pointsSumAllYear: String?,
|
||||
|
||||
val average: Double,
|
||||
|
||||
@ColumnInfo(name = "average_all_year")
|
||||
val averageAllYear: Double? = null,
|
||||
) {
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
var id: Long = 0
|
||||
|
@ -9,8 +9,8 @@ import java.time.Instant
|
||||
@Entity(tableName = "MobileDevices")
|
||||
data class MobileDevice(
|
||||
|
||||
@ColumnInfo(name = "user_login_id")
|
||||
val userLoginId: Int,
|
||||
@ColumnInfo(name = "user_login_id") // todo: change column name
|
||||
val studentId: Int,
|
||||
|
||||
@ColumnInfo(name = "device_id")
|
||||
val deviceId: Int,
|
||||
|
@ -9,8 +9,8 @@ import java.time.LocalDate
|
||||
@Entity(tableName = "SchoolAnnouncements")
|
||||
data class SchoolAnnouncement(
|
||||
|
||||
@ColumnInfo(name = "user_login_id")
|
||||
val userLoginId: Int,
|
||||
@ColumnInfo(name = "user_login_id") // todo: change column name
|
||||
val studentId: Int,
|
||||
|
||||
val date: LocalDate,
|
||||
|
||||
|
@ -49,6 +49,7 @@ data class Student(
|
||||
@ColumnInfo(name = "student_id")
|
||||
val studentId: Int,
|
||||
|
||||
@Deprecated("not available in VULCAN anymore")
|
||||
@ColumnInfo(name = "user_login_id")
|
||||
val userLoginId: Int,
|
||||
|
||||
@ -82,8 +83,8 @@ data class Student(
|
||||
@ColumnInfo(name = "is_authorized", defaultValue = "0")
|
||||
val isAuthorized: Boolean,
|
||||
|
||||
@ColumnInfo(name = "is_edu_one", defaultValue = "0")
|
||||
val isEduOne: Boolean,
|
||||
@ColumnInfo(name = "is_edu_one", defaultValue = "NULL")
|
||||
val isEduOne: Boolean?,
|
||||
|
||||
) : Serializable {
|
||||
|
||||
@ -95,3 +96,22 @@ data class Student(
|
||||
@ColumnInfo(name = "avatar_color")
|
||||
var avatarColor = 0L
|
||||
}
|
||||
|
||||
@Entity
|
||||
data class StudentIsAuthorized(
|
||||
|
||||
@PrimaryKey
|
||||
var id: Long,
|
||||
|
||||
@ColumnInfo(name = "is_authorized", defaultValue = "NULL")
|
||||
val isAuthorized: Boolean?,
|
||||
) : Serializable
|
||||
|
||||
@Entity
|
||||
data class StudentIsEduOne(
|
||||
@PrimaryKey
|
||||
var id: Long,
|
||||
|
||||
@ColumnInfo(name = "is_edu_one", defaultValue = "NULL")
|
||||
val isEduOne: Boolean?,
|
||||
) : Serializable
|
||||
|
@ -1,16 +0,0 @@
|
||||
package io.github.wulkanowy.data.db.entities
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import java.io.Serializable
|
||||
|
||||
@Entity
|
||||
data class StudentIsAuthorized(
|
||||
@ColumnInfo(name = "is_authorized", defaultValue = "0")
|
||||
val isAuthorized: Boolean,
|
||||
) : Serializable {
|
||||
|
||||
@PrimaryKey
|
||||
var id: Long = 0
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
package io.github.wulkanowy.data.db.migrations
|
||||
|
||||
import androidx.room.migration.AutoMigrationSpec
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
|
||||
class Migration63 : AutoMigrationSpec {
|
||||
|
||||
override fun onPostMigrate(db: SupportSQLiteDatabase) {
|
||||
db.execSQL("UPDATE Students SET is_edu_one = NULL WHERE is_edu_one = 0")
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package io.github.wulkanowy.data.enums
|
||||
|
||||
enum class AttendanceCalculatorSortingMode(private val value: String) {
|
||||
ALPHABETIC("alphabetic"),
|
||||
ATTENDANCE("attendance_percentage"),
|
||||
LESSON_BALANCE("lesson_balance");
|
||||
|
||||
companion object {
|
||||
fun getByValue(value: String) =
|
||||
AttendanceCalculatorSortingMode.values()
|
||||
.find { it.value == value } ?: ALPHABETIC
|
||||
}
|
||||
}
|
@ -4,6 +4,8 @@ enum class MessageType {
|
||||
GENERAL_MESSAGE,
|
||||
DASHBOARD_MESSAGE,
|
||||
LOGIN_MESSAGE,
|
||||
LOGIN_STUDENT_SELECT_MESSAGE,
|
||||
LOGIN_SYMBOL_MESSAGE,
|
||||
PASS_RESET_MESSAGE,
|
||||
ERROR_OVERRIDE,
|
||||
}
|
||||
|
@ -0,0 +1,11 @@
|
||||
package io.github.wulkanowy.data.enums
|
||||
|
||||
enum class ShowAdditionalLessonsMode(val value: String) {
|
||||
NONE("none"),
|
||||
INLINE("inline"),
|
||||
BELOW("below");
|
||||
|
||||
companion object {
|
||||
fun getByValue(value: String) = entries.find { it.value == value } ?: INLINE
|
||||
}
|
||||
}
|
@ -8,7 +8,7 @@ import io.github.wulkanowy.sdk.pojo.LastAnnouncement as SdkLastAnnouncement
|
||||
@JvmName("mapDirectorInformationToEntities")
|
||||
fun List<SdkDirectorInformation>.mapToEntities(student: Student) = map {
|
||||
SchoolAnnouncement(
|
||||
userLoginId = student.userLoginId,
|
||||
studentId = student.studentId,
|
||||
date = it.date,
|
||||
subject = it.subject,
|
||||
content = it.content,
|
||||
@ -19,7 +19,7 @@ fun List<SdkDirectorInformation>.mapToEntities(student: Student) = map {
|
||||
@JvmName("mapLastAnnouncementsToEntities")
|
||||
fun List<SdkLastAnnouncement>.mapToEntities(student: Student) = map {
|
||||
SchoolAnnouncement(
|
||||
userLoginId = student.userLoginId,
|
||||
studentId = student.studentId,
|
||||
date = it.date,
|
||||
subject = it.subject,
|
||||
content = it.content,
|
||||
|
@ -37,9 +37,11 @@ fun List<SdkGradeSummary>.mapToEntities(semester: Semester) = map {
|
||||
predictedGrade = it.predicted,
|
||||
finalGrade = it.final,
|
||||
pointsSum = it.pointsSum,
|
||||
pointsSumAllYear = it.pointsSumAllYear,
|
||||
proposedPoints = it.proposedPoints,
|
||||
finalPoints = it.finalPoints,
|
||||
average = it.average
|
||||
average = it.average,
|
||||
averageAllYear = it.averageAllYear,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -8,7 +8,7 @@ import io.github.wulkanowy.sdk.pojo.Token as SdkToken
|
||||
|
||||
fun List<SdkDevice>.mapToEntities(student: Student) = map {
|
||||
MobileDevice(
|
||||
userLoginId = student.userLoginId,
|
||||
studentId = student.studentId,
|
||||
date = it.createDate.toInstant(),
|
||||
deviceId = it.id,
|
||||
name = it.name
|
||||
|
@ -0,0 +1,14 @@
|
||||
package io.github.wulkanowy.data.pojos
|
||||
|
||||
data class AttendanceData(
|
||||
val subjectName: String,
|
||||
val lessonBalance: Int,
|
||||
val presences: Int,
|
||||
val absences: Int,
|
||||
) {
|
||||
val total: Int
|
||||
get() = presences + absences
|
||||
|
||||
val presencePercentage: Double
|
||||
get() = if (total == 0) 0.0 else (presences.toDouble() / total) * 100
|
||||
}
|
@ -6,6 +6,7 @@ import io.github.wulkanowy.data.db.dao.AdminMessageDao
|
||||
import io.github.wulkanowy.data.db.entities.AdminMessage
|
||||
import io.github.wulkanowy.data.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,14 +8,11 @@ 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.flow.Flow
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
@ -28,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,
|
||||
) {
|
||||
|
||||
@ -59,8 +57,7 @@ class AttendanceRepository @Inject constructor(
|
||||
val lessons = timetableDb.load(
|
||||
semester.diaryId, semester.studentId, start.monday, end.sunday
|
||||
)
|
||||
sdk.init(student)
|
||||
.switchSemester(semester)
|
||||
wulkanowySdkFactory.create(student, semester)
|
||||
.getAttendance(start.monday, end.sunday)
|
||||
.mapToEntities(semester, lessons)
|
||||
},
|
||||
@ -90,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(
|
||||
@ -99,8 +98,7 @@ class AttendanceRepository @Inject constructor(
|
||||
timeId = attendance.timeId
|
||||
)
|
||||
}
|
||||
sdk.init(student)
|
||||
.switchSemester(semester)
|
||||
wulkanowySdkFactory.create(student, semester)
|
||||
.excuseForAbsence(items, reason)
|
||||
}
|
||||
}
|
||||
|
@ -1,17 +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
|
||||
@ -20,9 +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()
|
||||
@ -43,8 +41,7 @@ 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)
|
||||
},
|
||||
|
@ -1,17 +1,15 @@
|
||||
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.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
|
||||
@ -21,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,
|
||||
) {
|
||||
|
||||
@ -53,8 +51,7 @@ class CompletedLessonsRepository @Inject constructor(
|
||||
)
|
||||
},
|
||||
fetch = {
|
||||
sdk.init(student)
|
||||
.switchSemester(semester)
|
||||
wulkanowySdkFactory.create(student, semester)
|
||||
.getCompletedLessons(start.monday, end.sunday)
|
||||
.mapToEntities(semester)
|
||||
},
|
||||
|
@ -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,8 +44,7 @@ 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 }
|
||||
|
@ -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,8 +54,7 @@ class ExamRepository @Inject constructor(
|
||||
)
|
||||
},
|
||||
fetch = {
|
||||
sdk.init(student)
|
||||
.switchSemester(semester)
|
||||
wulkanowySdkFactory.create(student, semester)
|
||||
.getExams(start.startExamsDay, start.endExamsDay)
|
||||
.mapToEntities(semester)
|
||||
},
|
||||
|
@ -1,5 +1,6 @@
|
||||
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
|
||||
@ -10,11 +11,8 @@ 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.toLocalDate
|
||||
import io.github.wulkanowy.utils.uniqueSubtract
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
@ -30,7 +28,7 @@ class GradeRepository @Inject constructor(
|
||||
private val gradeDb: GradeDao,
|
||||
private val gradeSummaryDb: GradeSummaryDao,
|
||||
private val gradeDescriptiveDb: GradeDescriptiveDao,
|
||||
private val sdk: Sdk,
|
||||
private val wulkanowySdkFactory: WulkanowySdkFactory,
|
||||
private val refreshHelper: AutoRefreshHelper,
|
||||
) {
|
||||
|
||||
@ -63,8 +61,7 @@ class GradeRepository @Inject constructor(
|
||||
}
|
||||
},
|
||||
fetch = {
|
||||
val (details, summary, descriptive) = sdk.init(student)
|
||||
.switchSemester(semester)
|
||||
val (details, summary, descriptive) = wulkanowySdkFactory.create(student, semester)
|
||||
.getGrades(semester.semesterId)
|
||||
|
||||
Triple(
|
||||
|
@ -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,11 +13,8 @@ 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.Locale
|
||||
@ -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,8 +54,7 @@ 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)
|
||||
},
|
||||
@ -104,8 +101,7 @@ 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)
|
||||
},
|
||||
@ -163,8 +159,7 @@ 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)
|
||||
},
|
||||
|
@ -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,8 +53,7 @@ class HomeworkRepository @Inject constructor(
|
||||
)
|
||||
},
|
||||
fetch = {
|
||||
sdk.init(student)
|
||||
.switchSemester(semester)
|
||||
wulkanowySdkFactory.create(student, semester)
|
||||
.getHomework(start.monday, end.sunday)
|
||||
.mapToEntities(semester)
|
||||
},
|
||||
|
@ -1,12 +1,13 @@
|
||||
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 io.github.wulkanowy.ui.modules.luckynumberwidget.LuckyNumberWidgetProvider
|
||||
import io.github.wulkanowy.utils.AppWidgetUpdater
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
@ -18,7 +19,8 @@ import javax.inject.Singleton
|
||||
@Singleton
|
||||
class LuckyNumberRepository @Inject constructor(
|
||||
private val luckyNumberDb: LuckyNumberDao,
|
||||
private val sdk: Sdk,
|
||||
private val wulkanowySdkFactory: WulkanowySdkFactory,
|
||||
private val appWidgetUpdater: AppWidgetUpdater,
|
||||
) {
|
||||
|
||||
private val saveFetchResultMutex = Mutex()
|
||||
@ -27,13 +29,16 @@ class LuckyNumberRepository @Inject constructor(
|
||||
student: Student,
|
||||
forceRefresh: Boolean,
|
||||
notify: Boolean = false,
|
||||
isFromAppWidget: Boolean = false
|
||||
) = networkBoundResource(
|
||||
mutex = saveFetchResultMutex,
|
||||
isResultEmpty = { it == null },
|
||||
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
|
||||
@ -43,6 +48,9 @@ class LuckyNumberRepository @Inject constructor(
|
||||
oldItems = listOfNotNull(oldLuckyNumber),
|
||||
newItems = listOf(newLuckyNumber.apply { if (notify) isNotified = false }),
|
||||
)
|
||||
if (!isFromAppWidget) {
|
||||
appWidgetUpdater.updateAllAppWidgetsByProvider(LuckyNumberWidgetProvider::class)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
@ -4,6 +4,7 @@ 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
|
||||
@ -29,11 +30,9 @@ 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.sync.Mutex
|
||||
@ -48,7 +47,7 @@ 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,
|
||||
@ -82,10 +81,16 @@ class MessageRepository @Inject constructor(
|
||||
} 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 = { oldWithAuthors, new ->
|
||||
val old = oldWithAuthors.map { it.message }
|
||||
@ -115,10 +120,11 @@ class MessageRepository @Inject constructor(
|
||||
},
|
||||
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!" }
|
||||
@ -159,19 +165,19 @@ class MessageRepository @Inject constructor(
|
||||
recipients: List<Recipient>,
|
||||
mailbox: Mailbox,
|
||||
) {
|
||||
sdk.init(student).sendMessage(
|
||||
subject = subject,
|
||||
content = content,
|
||||
recipients = recipients.mapFromEntities(),
|
||||
mailboxId = mailbox.globalKey,
|
||||
)
|
||||
wulkanowySdkFactory.create(student)
|
||||
.sendMessage(
|
||||
subject = subject,
|
||||
content = content,
|
||||
recipients = recipients.mapFromEntities(),
|
||||
mailboxId = mailbox.globalKey,
|
||||
)
|
||||
refreshFolders(student, mailbox, listOf(SENT))
|
||||
}
|
||||
|
||||
suspend fun restoreMessages(student: Student, mailbox: Mailbox?, messages: List<Message>) {
|
||||
sdk.init(student).restoreMessages(
|
||||
messages = messages.map { it.messageGlobalKey },
|
||||
)
|
||||
wulkanowySdkFactory.create(student)
|
||||
.restoreMessages(messages = messages.map { it.messageGlobalKey })
|
||||
|
||||
refreshFolders(student, mailbox)
|
||||
}
|
||||
@ -182,10 +188,11 @@ class MessageRepository @Inject constructor(
|
||||
|
||||
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 {
|
||||
@ -230,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)
|
||||
|
@ -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,
|
||||
) {
|
||||
|
||||
@ -40,10 +38,9 @@ class MobileDeviceRepository @Inject constructor(
|
||||
val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, student))
|
||||
it.isEmpty() || forceRefresh || isExpired
|
||||
},
|
||||
query = { mobileDb.loadAll(student.userLoginId) },
|
||||
query = { mobileDb.loadAll(student.studentId) },
|
||||
fetch = {
|
||||
sdk.init(student)
|
||||
.switchSemester(semester)
|
||||
wulkanowySdkFactory.create(student, semester)
|
||||
.getRegisteredDevices()
|
||||
.mapToEntities(student)
|
||||
},
|
||||
@ -57,16 +54,14 @@ class MobileDeviceRepository @Inject constructor(
|
||||
)
|
||||
|
||||
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,16 +1,14 @@
|
||||
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.AutoRefreshHelper
|
||||
import io.github.wulkanowy.utils.getRefreshKey
|
||||
import io.github.wulkanowy.utils.init
|
||||
import io.github.wulkanowy.utils.switchSemester
|
||||
import io.github.wulkanowy.utils.toLocalDate
|
||||
import io.github.wulkanowy.utils.uniqueSubtract
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
@ -21,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,
|
||||
) {
|
||||
|
||||
@ -45,8 +43,7 @@ class NoteRepository @Inject constructor(
|
||||
},
|
||||
query = { noteDb.loadAll(student.studentId) },
|
||||
fetch = {
|
||||
sdk.init(student)
|
||||
.switchSemester(semester)
|
||||
wulkanowySdkFactory.create(student, semester)
|
||||
.getNotes()
|
||||
.mapToEntities(semester)
|
||||
},
|
||||
|
@ -10,9 +10,11 @@ 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
|
||||
import io.github.wulkanowy.data.enums.ShowAdditionalLessonsMode
|
||||
import io.github.wulkanowy.data.enums.TimetableGapsMode
|
||||
import io.github.wulkanowy.data.enums.TimetableMode
|
||||
import io.github.wulkanowy.ui.modules.dashboard.DashboardItem
|
||||
@ -41,6 +43,27 @@ class PreferencesRepository @Inject constructor(
|
||||
R.bool.pref_default_attendance_present
|
||||
)
|
||||
|
||||
val targetAttendanceFlow: Flow<Int>
|
||||
get() = flowSharedPref.getInt(
|
||||
context.getString(R.string.pref_key_attendance_target),
|
||||
context.resources.getInteger(R.integer.pref_default_attendance_target)
|
||||
).asFlow()
|
||||
|
||||
val attendanceCalculatorSortingModeFlow: Flow<AttendanceCalculatorSortingMode>
|
||||
get() = flowSharedPref.getString(
|
||||
context.getString(R.string.pref_key_attendance_calculator_sorting_mode),
|
||||
context.resources.getString(R.string.pref_default_attendance_calculator_sorting_mode)
|
||||
).asFlow().map(AttendanceCalculatorSortingMode::getByValue)
|
||||
|
||||
/**
|
||||
* Subjects are empty when they don't have any attendances (total = 0, attendances = 0, absences = 0).
|
||||
*/
|
||||
val attendanceCalculatorShowEmptySubjects: Flow<Boolean>
|
||||
get() = flowSharedPref.getBoolean(
|
||||
context.getString(R.string.pref_key_attendance_calculator_show_empty_subjects),
|
||||
context.resources.getBoolean(R.bool.pref_default_attendance_calculator_show_empty_subjects)
|
||||
).asFlow()
|
||||
|
||||
private val gradeAverageModePref: Preference<GradeAverageMode>
|
||||
get() = getObjectFlow(
|
||||
R.string.pref_key_grade_average_mode,
|
||||
@ -191,6 +214,12 @@ class PreferencesRepository @Inject constructor(
|
||||
)
|
||||
)
|
||||
|
||||
val showAdditionalLessonsInPlan: ShowAdditionalLessonsMode
|
||||
get() = getString(
|
||||
R.string.pref_key_timetable_show_additional_lessons,
|
||||
R.string.pref_default_timetable_show_additional_lessons
|
||||
).let { ShowAdditionalLessonsMode.getByValue(it) }
|
||||
|
||||
val gradeSortingMode: GradeSortingMode
|
||||
get() = GradeSortingMode.getByValue(
|
||||
getString(
|
||||
|
@ -1,5 +1,6 @@
|
||||
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.Mailbox
|
||||
import io.github.wulkanowy.data.db.entities.MailboxType
|
||||
@ -7,10 +8,8 @@ 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
|
||||
@ -18,14 +17,15 @@ 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)
|
||||
|
||||
@ -60,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,
|
||||
) {
|
||||
|
||||
@ -38,10 +37,10 @@ class SchoolAnnouncementRepository @Inject constructor(
|
||||
it.isEmpty() || forceRefresh || isExpired
|
||||
},
|
||||
query = {
|
||||
schoolAnnouncementDb.loadAll(student.userLoginId)
|
||||
schoolAnnouncementDb.loadAll(student.studentId)
|
||||
},
|
||||
fetch = {
|
||||
val sdk = sdk.init(student)
|
||||
val sdk = wulkanowySdkFactory.create(student)
|
||||
val lastAnnouncements = sdk.getLastAnnouncements().mapToEntities(student)
|
||||
val directorInformation = sdk.getDirectorInformation().mapToEntities(student)
|
||||
lastAnnouncements + directorInformation
|
||||
@ -58,7 +57,7 @@ class SchoolAnnouncementRepository @Inject constructor(
|
||||
)
|
||||
|
||||
fun getSchoolAnnouncementFromDatabase(student: Student): Flow<List<SchoolAnnouncement>> {
|
||||
return schoolAnnouncementDb.loadAll(student.userLoginId)
|
||||
return schoolAnnouncementDb.loadAll(student.studentId)
|
||||
}
|
||||
|
||||
suspend fun updateSchoolAnnouncement(schoolAnnouncement: List<SchoolAnnouncement>) =
|
||||
|
@ -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,8 +38,7 @@ class SchoolRepository @Inject constructor(
|
||||
},
|
||||
query = { schoolDb.load(semester.studentId, semester.classId) },
|
||||
fetch = {
|
||||
sdk.init(student)
|
||||
.switchSemester(semester)
|
||||
wulkanowySdkFactory.create(student, semester)
|
||||
.getSchool()
|
||||
.mapToEntity(semester)
|
||||
},
|
||||
|
@ -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,5 +1,6 @@
|
||||
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
|
||||
@ -7,7 +8,6 @@ import io.github.wulkanowy.data.mappers.mapToEntities
|
||||
import io.github.wulkanowy.sdk.Sdk
|
||||
import io.github.wulkanowy.utils.DispatchersProvider
|
||||
import io.github.wulkanowy.utils.getCurrentOrLast
|
||||
import io.github.wulkanowy.utils.init
|
||||
import io.github.wulkanowy.utils.isCurrent
|
||||
import io.github.wulkanowy.utils.uniqueSubtract
|
||||
import kotlinx.coroutines.withContext
|
||||
@ -18,7 +18,7 @@ import javax.inject.Singleton
|
||||
@Singleton
|
||||
class SemesterRepository @Inject constructor(
|
||||
private val semesterDb: SemesterDao,
|
||||
private val sdk: Sdk,
|
||||
private val wulkanowySdkFactory: WulkanowySdkFactory,
|
||||
private val dispatchers: DispatchersProvider,
|
||||
) {
|
||||
|
||||
@ -60,8 +60,14 @@ class SemesterRepository @Inject constructor(
|
||||
}
|
||||
|
||||
private suspend fun refreshSemesters(student: Student) {
|
||||
val new = sdk.init(student).getSemesters().mapToEntities(student.studentId)
|
||||
if (new.isEmpty()) return Timber.i("Empty semester list!")
|
||||
val new = wulkanowySdkFactory.create(student)
|
||||
.getSemesters()
|
||||
.mapToEntities(student.studentId)
|
||||
|
||||
if (new.isEmpty()) {
|
||||
Timber.i("Empty semester list from SDK!")
|
||||
return
|
||||
}
|
||||
|
||||
val old = semesterDb.loadAll(student.studentId, student.classId)
|
||||
semesterDb.removeOldAndSaveNew(
|
||||
|
@ -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,9 +28,9 @@ 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) {
|
||||
|
@ -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
|
||||
@ -11,15 +12,14 @@ import io.github.wulkanowy.data.db.entities.StudentName
|
||||
import io.github.wulkanowy.data.db.entities.StudentNickAndAvatar
|
||||
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
|
||||
import io.github.wulkanowy.data.exceptions.NoCurrentStudentException
|
||||
import io.github.wulkanowy.data.mappers.mapToEntities
|
||||
import io.github.wulkanowy.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.getCurrentOrLast
|
||||
import io.github.wulkanowy.utils.init
|
||||
import io.github.wulkanowy.utils.security.Scrambler
|
||||
import io.github.wulkanowy.utils.switchSemester
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@ -28,7 +28,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,
|
||||
) {
|
||||
@ -39,9 +39,10 @@ class StudentRepository @Inject constructor(
|
||||
pin: String,
|
||||
symbol: String,
|
||||
token: String
|
||||
): RegisterUser = sdk
|
||||
): RegisterUser = wulkanowySdkFactory.create()
|
||||
.getStudentsFromHebe(token, pin, symbol, "")
|
||||
.mapToPojo(null)
|
||||
.also { it.logErrors() }
|
||||
|
||||
suspend fun getUserSubjectsFromScrapper(
|
||||
email: String,
|
||||
@ -49,18 +50,20 @@ class StudentRepository @Inject constructor(
|
||||
scrapperBaseUrl: String,
|
||||
domainSuffix: String,
|
||||
symbol: String
|
||||
): RegisterUser = sdk
|
||||
): RegisterUser = wulkanowySdkFactory.create()
|
||||
.getUserSubjectsFromScrapper(email, password, scrapperBaseUrl, domainSuffix, symbol)
|
||||
.mapToPojo(password)
|
||||
.also { it.logErrors() }
|
||||
|
||||
suspend fun getStudentsHybrid(
|
||||
email: String,
|
||||
password: String,
|
||||
scrapperBaseUrl: String,
|
||||
symbol: String
|
||||
): RegisterUser = sdk
|
||||
): RegisterUser = wulkanowySdkFactory.create()
|
||||
.getStudentsHybrid(email, password, scrapperBaseUrl, "", symbol)
|
||||
.mapToPojo(password)
|
||||
.also { it.logErrors() }
|
||||
|
||||
suspend fun getSavedStudents(decryptPass: Boolean = true): List<StudentWithSemesters> {
|
||||
return studentDb.loadStudentsWithSemesters().map { (student, semesters) ->
|
||||
@ -102,23 +105,44 @@ class StudentRepository @Inject constructor(
|
||||
return student
|
||||
}
|
||||
|
||||
suspend fun checkCurrentStudentAuthorizationStatus() {
|
||||
suspend fun updateCurrentStudentAuthStatus() {
|
||||
Timber.i("Check isAuthorized: started")
|
||||
val student = getCurrentStudent()
|
||||
|
||||
if (!student.isAuthorized) {
|
||||
val currentSemester = semesterDb.loadAll(
|
||||
studentId = student.studentId,
|
||||
classId = student.classId,
|
||||
).getCurrentOrLast()
|
||||
val initializedSdk = sdk.init(student).switchSemester(currentSemester)
|
||||
val isAuthorized = initializedSdk.getCurrentStudent()?.isAuthorized ?: false
|
||||
|
||||
if (isAuthorized) {
|
||||
studentDb.update(StudentIsAuthorized(isAuthorized = true).apply {
|
||||
id = student.id
|
||||
})
|
||||
} else throw NoAuthorizationException()
|
||||
if (student.isAuthorized) {
|
||||
Timber.i("Check isAuthorized: already authorized")
|
||||
return
|
||||
}
|
||||
|
||||
val initializedSdk = wulkanowySdkFactory.create(student)
|
||||
val newCurrentStudent = runCatching { initializedSdk.getCurrentStudent() }
|
||||
.onFailure { Timber.e(it, "Check isAuthorized: error occurred") }
|
||||
.getOrNull()
|
||||
|
||||
if (newCurrentStudent == null) {
|
||||
Timber.d("Check isAuthorized: current user is null")
|
||||
return
|
||||
}
|
||||
|
||||
val currentStudentSemesters = semesterDb.loadAll(student.studentId, student.classId)
|
||||
if (currentStudentSemesters.isEmpty()) {
|
||||
Timber.d("Check isAuthorized: apply empty semesters workaround")
|
||||
semesterDb.insertSemesters(
|
||||
items = newCurrentStudent.semesters.mapToEntities(student.studentId),
|
||||
)
|
||||
}
|
||||
|
||||
if (!newCurrentStudent.isAuthorized) {
|
||||
Timber.i("Check isAuthorized: authorization required")
|
||||
throw NoAuthorizationException()
|
||||
}
|
||||
|
||||
val studentIsAuthorized = StudentIsAuthorized(
|
||||
id = student.id,
|
||||
isAuthorized = true
|
||||
)
|
||||
|
||||
Timber.i("Check isAuthorized: already authorized, update local status")
|
||||
studentDb.update(studentIsAuthorized)
|
||||
}
|
||||
|
||||
suspend fun getCurrentStudent(decryptPass: Boolean = true): Student {
|
||||
@ -170,20 +194,24 @@ 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)
|
||||
.getCurrentStudent() ?: return
|
||||
suspend fun refreshStudentAfterAuthorize(student: Student, semester: Semester) {
|
||||
val wulkanowySdk = wulkanowySdkFactory.create(student, semester)
|
||||
val newCurrentApiStudent = runCatching { wulkanowySdk.getCurrentStudent() }
|
||||
.onFailure { Timber.e(it, "Can't find student with id ${student.studentId}") }
|
||||
.getOrNull() ?: return
|
||||
|
||||
val studentName = StudentName(
|
||||
studentName = "${newCurrentApiStudent.studentName} ${newCurrentApiStudent.studentSurname}"
|
||||
).apply { id = student.id }
|
||||
|
||||
studentDb.update(studentName)
|
||||
semesterDb.removeOldAndSaveNew(
|
||||
oldItems = semesterDb.loadAll(student.studentId, semester.classId),
|
||||
newItems = newCurrentApiStudent.semesters.mapToEntities(newCurrentApiStudent.studentId)
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun deleteStudentsAssociatedWithAccount(student: Student) {
|
||||
@ -196,7 +224,18 @@ class StudentRepository @Inject constructor(
|
||||
appDatabase.clearAllTables()
|
||||
}
|
||||
}
|
||||
|
||||
private fun RegisterUser.logErrors() {
|
||||
val symbolsErrors = symbols.filter { it.error != null }
|
||||
.map { it.error }
|
||||
val unitsErrors = symbols.flatMap { it.schools }
|
||||
.filter { it.error != null }
|
||||
.map { it.error }
|
||||
|
||||
(symbolsErrors + unitsErrors).forEach { error ->
|
||||
Timber.e(error, "Error occurred while fetching students")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class NoAuthorizationException : Exception()
|
||||
|
||||
|
@ -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,8 +37,7 @@ class SubjectRepository @Inject constructor(
|
||||
},
|
||||
query = { subjectDao.loadAll(semester.diaryId, semester.studentId) },
|
||||
fetch = {
|
||||
sdk.init(student)
|
||||
.switchSemester(semester)
|
||||
wulkanowySdkFactory.create(student, semester)
|
||||
.getSubjects()
|
||||
.mapToEntities(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,8 +37,7 @@ class TeacherRepository @Inject constructor(
|
||||
},
|
||||
query = { teacherDb.loadAll(semester.studentId, semester.classId) },
|
||||
fetch = {
|
||||
sdk.init(student)
|
||||
.switchSemester(semester)
|
||||
wulkanowySdkFactory.create(student, semester)
|
||||
.getTeachers()
|
||||
.mapToEntities(semester)
|
||||
},
|
||||
|
@ -1,5 +1,6 @@
|
||||
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
|
||||
@ -11,14 +12,13 @@ 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.ui.modules.timetablewidget.TimetableWidgetProvider
|
||||
import io.github.wulkanowy.utils.AppWidgetUpdater
|
||||
import io.github.wulkanowy.utils.AutoRefreshHelper
|
||||
import io.github.wulkanowy.utils.getRefreshKey
|
||||
import io.github.wulkanowy.utils.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.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
@ -28,14 +28,16 @@ import java.time.LocalDate
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
||||
@Singleton
|
||||
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,
|
||||
private val appWidgetUpdater: AppWidgetUpdater,
|
||||
) {
|
||||
|
||||
private val saveFetchResultMutex = Mutex()
|
||||
@ -54,7 +56,8 @@ class TimetableRepository @Inject constructor(
|
||||
forceRefresh: Boolean,
|
||||
refreshAdditional: Boolean = false,
|
||||
notify: Boolean = false,
|
||||
timetableType: TimetableType = TimetableType.NORMAL
|
||||
timetableType: TimetableType = TimetableType.NORMAL,
|
||||
isFromAppWidget: Boolean = false
|
||||
) = networkBoundResource(
|
||||
mutex = saveFetchResultMutex,
|
||||
isResultEmpty = {
|
||||
@ -74,8 +77,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)
|
||||
@ -86,6 +88,9 @@ class TimetableRepository @Inject constructor(
|
||||
refreshDayHeaders(timetableOld.headers, timetableNew.headers)
|
||||
|
||||
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester, start, end))
|
||||
if (!isFromAppWidget) {
|
||||
appWidgetUpdater.updateAllAppWidgetsByProvider(TimetableWidgetProvider::class)
|
||||
}
|
||||
},
|
||||
filterResult = { (timetable, additional, headers) ->
|
||||
TimetableFull(
|
||||
|
@ -0,0 +1,27 @@
|
||||
package io.github.wulkanowy.data.serializers
|
||||
|
||||
import io.github.wulkanowy.data.enums.MessageType
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.builtins.ListSerializer
|
||||
import kotlinx.serialization.builtins.serializer
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
object SafeMessageTypeEnumListSerializer : KSerializer<List<MessageType>> {
|
||||
|
||||
private val serializer = ListSerializer(String.serializer())
|
||||
|
||||
override val descriptor = serializer.descriptor
|
||||
|
||||
override fun serialize(encoder: Encoder, value: List<MessageType>) {
|
||||
encoder.encodeNotNullMark()
|
||||
serializer.serialize(encoder, value.map { it.name })
|
||||
}
|
||||
|
||||
override fun deserialize(decoder: Decoder): List<MessageType> =
|
||||
serializer.deserialize(decoder).mapNotNull { enumName ->
|
||||
MessageType.entries.find { it.name == enumName }
|
||||
}
|
||||
}
|
@ -0,0 +1,106 @@
|
||||
package io.github.wulkanowy.domain.attendance
|
||||
|
||||
import io.github.wulkanowy.data.*
|
||||
import io.github.wulkanowy.data.db.entities.AttendanceSummary
|
||||
import io.github.wulkanowy.data.db.entities.Semester
|
||||
import io.github.wulkanowy.data.db.entities.Student
|
||||
import io.github.wulkanowy.data.db.entities.Subject
|
||||
import io.github.wulkanowy.data.enums.AttendanceCalculatorSortingMode
|
||||
import io.github.wulkanowy.data.enums.AttendanceCalculatorSortingMode.*
|
||||
import io.github.wulkanowy.data.pojos.AttendanceData
|
||||
import io.github.wulkanowy.data.repositories.AttendanceSummaryRepository
|
||||
import io.github.wulkanowy.data.repositories.PreferencesRepository
|
||||
import io.github.wulkanowy.data.repositories.SubjectRepository
|
||||
import io.github.wulkanowy.utils.allAbsences
|
||||
import io.github.wulkanowy.utils.allPresences
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.floor
|
||||
|
||||
class GetAttendanceCalculatorDataUseCase @Inject constructor(
|
||||
private val subjectRepository: SubjectRepository,
|
||||
private val attendanceSummaryRepository: AttendanceSummaryRepository,
|
||||
private val preferencesRepository: PreferencesRepository,
|
||||
) {
|
||||
|
||||
operator fun invoke(
|
||||
student: Student,
|
||||
semester: Semester,
|
||||
forceRefresh: Boolean,
|
||||
): Flow<Resource<List<AttendanceData>>> =
|
||||
subjectRepository.getSubjects(student, semester, forceRefresh)
|
||||
.mapResourceData { subjects -> subjects.sortedBy(Subject::name) }
|
||||
.combineWithResourceData(preferencesRepository.targetAttendanceFlow, ::Pair)
|
||||
.flatMapResourceData { (subjects, targetFreq) ->
|
||||
combineResourceFlows(subjects.map { subject ->
|
||||
attendanceSummaryRepository.getAttendanceSummary(
|
||||
student = student,
|
||||
semester = semester,
|
||||
subjectId = subject.realId,
|
||||
forceRefresh = forceRefresh
|
||||
).mapResourceData { summaries ->
|
||||
summaries.toAttendanceData(subject.name, targetFreq)
|
||||
}
|
||||
})
|
||||
// Every individual combined flow causes separate network requests to update data.
|
||||
// When there is N child flows, they can cause up to N-1 items to be emitted. Since all
|
||||
// requests are usually completed in less than 5s, there is no need to emit multiple
|
||||
// intermediates that will be visible for barely any time.
|
||||
.debounceIntermediates()
|
||||
}
|
||||
.combineWithResourceData(preferencesRepository.attendanceCalculatorShowEmptySubjects) { attendanceDataList, showEmptySubjects ->
|
||||
attendanceDataList.filter { it.total != 0 || showEmptySubjects }
|
||||
}
|
||||
.combineWithResourceData(preferencesRepository.attendanceCalculatorSortingModeFlow, List<AttendanceData>::sortedBy)
|
||||
}
|
||||
|
||||
private fun List<AttendanceSummary>.toAttendanceData(subjectName: String, targetFreq: Int): AttendanceData {
|
||||
val presences = sumOf { it.allPresences }
|
||||
val absences = sumOf { it.allAbsences }
|
||||
return AttendanceData(
|
||||
subjectName = subjectName,
|
||||
lessonBalance = calcLessonBalance(
|
||||
targetFreq.toDouble() / 100, presences, absences
|
||||
),
|
||||
presences = presences,
|
||||
absences = absences,
|
||||
)
|
||||
}
|
||||
|
||||
private fun calcLessonBalance(targetFreq: Double, presences: Int, absences: Int): Int {
|
||||
val total = presences + absences
|
||||
// The `+ 1` is to avoid false positives in close cases. Eg.:
|
||||
// target frequency 99%, 1 presence. Without the `+ 1` this would be reported shown as
|
||||
// a positive balance of +1, however that is not actually true as skipping one class
|
||||
// would make it so that the balance would actually be negative (-98). The `+ 1`
|
||||
// fixes this and makes sure that in situations like these, it's not reporting incorrect
|
||||
// balances
|
||||
return when {
|
||||
presences / (total + 1f) >= targetFreq -> calcMissingAbsences(
|
||||
targetFreq, absences, presences
|
||||
)
|
||||
presences / (total + 0f) < targetFreq -> -calcMissingPresences(
|
||||
targetFreq, absences, presences
|
||||
)
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
|
||||
private fun calcMissingPresences(targetFreq: Double, absences: Int, presences: Int) =
|
||||
calcMinRequiredPresencesFor(targetFreq, absences) - presences
|
||||
|
||||
private fun calcMinRequiredPresencesFor(targetFreq: Double, absences: Int) =
|
||||
ceil((targetFreq / (1 - targetFreq)) * absences).toInt()
|
||||
|
||||
private fun calcMissingAbsences(targetFreq: Double, absences: Int, presences: Int) =
|
||||
calcMinRequiredAbsencesFor(targetFreq, presences) - absences
|
||||
|
||||
private fun calcMinRequiredAbsencesFor(targetFreq: Double, presences: Int) =
|
||||
floor((presences * (1 - targetFreq)) / targetFreq).toInt()
|
||||
|
||||
private fun List<AttendanceData>.sortedBy(mode: AttendanceCalculatorSortingMode) = when (mode) {
|
||||
ALPHABETIC -> sortedBy(AttendanceData::subjectName)
|
||||
ATTENDANCE -> sortedByDescending(AttendanceData::presencePercentage)
|
||||
LESSON_BALANCE -> sortedBy(AttendanceData::lessonBalance)
|
||||
}
|
@ -59,7 +59,7 @@ class GetMailboxByStudentUseCase @Inject constructor(
|
||||
private fun String.getUnauthorizedVersion(): String {
|
||||
return normalizeStudentName().split(" ")
|
||||
.joinToString(" ") {
|
||||
it.first() + "*".repeat(it.length - 1)
|
||||
it.firstOrNull()?.toString().orEmpty() + "*".repeat((it.length - 1).coerceAtLeast(0))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ import io.github.wulkanowy.data.repositories.SemesterRepository
|
||||
import io.github.wulkanowy.data.repositories.StudentRepository
|
||||
import io.github.wulkanowy.sdk.exception.FeatureNotAvailableException
|
||||
import io.github.wulkanowy.sdk.scrapper.exception.FeatureDisabledException
|
||||
import io.github.wulkanowy.sdk.scrapper.exception.FeatureUnavailableException
|
||||
import io.github.wulkanowy.services.sync.channels.DebugChannel
|
||||
import io.github.wulkanowy.services.sync.works.Work
|
||||
import io.github.wulkanowy.utils.DispatchersProvider
|
||||
@ -48,6 +49,7 @@ class SyncWorker @AssistedInject constructor(
|
||||
val semester = semesterRepository.getCurrentSemester(student, true)
|
||||
student to semester
|
||||
} catch (e: Throwable) {
|
||||
Timber.e(e)
|
||||
return@withContext getResultFromErrors(listOf(e))
|
||||
}
|
||||
|
||||
@ -59,7 +61,7 @@ class SyncWorker @AssistedInject constructor(
|
||||
null
|
||||
} catch (e: Throwable) {
|
||||
Timber.w("${work::class.java.simpleName} result: An exception ${e.message} occurred")
|
||||
if (e is FeatureDisabledException || e is FeatureNotAvailableException) {
|
||||
if (e is FeatureDisabledException || e is FeatureNotAvailableException || e is FeatureUnavailableException) {
|
||||
null
|
||||
} else {
|
||||
Timber.e(e)
|
||||
|
@ -14,6 +14,7 @@ import io.github.wulkanowy.data.db.entities.Attendance
|
||||
import io.github.wulkanowy.databinding.DialogExcuseBinding
|
||||
import io.github.wulkanowy.databinding.FragmentAttendanceBinding
|
||||
import io.github.wulkanowy.ui.base.BaseFragment
|
||||
import io.github.wulkanowy.ui.modules.attendance.calculator.AttendanceCalculatorFragment
|
||||
import io.github.wulkanowy.ui.modules.attendance.summary.AttendanceSummaryFragment
|
||||
import io.github.wulkanowy.ui.modules.main.MainActivity
|
||||
import io.github.wulkanowy.ui.modules.main.MainView
|
||||
@ -134,6 +135,7 @@ class AttendanceFragment : BaseFragment<FragmentAttendanceBinding>(R.layout.frag
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return if (item.itemId == R.id.attendanceMenuSummary) presenter.onSummarySwitchSelected()
|
||||
else if (item.itemId == R.id.attendanceMenuCalculator) presenter.onCalculatorSwitchSelected()
|
||||
else false
|
||||
}
|
||||
|
||||
@ -253,6 +255,10 @@ class AttendanceFragment : BaseFragment<FragmentAttendanceBinding>(R.layout.frag
|
||||
(activity as? MainActivity)?.pushView(AttendanceSummaryFragment.newInstance())
|
||||
}
|
||||
|
||||
override fun openCalculatorView() {
|
||||
(activity as? MainActivity)?.pushView(AttendanceCalculatorFragment.newInstance())
|
||||
}
|
||||
|
||||
override fun startActionMode() {
|
||||
actionMode = (activity as MainActivity?)?.startSupportActionMode(actionModeCallback)
|
||||
}
|
||||
|
@ -1,16 +1,36 @@
|
||||
package io.github.wulkanowy.ui.modules.attendance
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import io.github.wulkanowy.data.*
|
||||
import io.github.wulkanowy.data.Resource
|
||||
import io.github.wulkanowy.data.db.entities.Attendance
|
||||
import io.github.wulkanowy.data.db.entities.Semester
|
||||
import io.github.wulkanowy.data.flatResourceFlow
|
||||
import io.github.wulkanowy.data.logResourceStatus
|
||||
import io.github.wulkanowy.data.mapResourceData
|
||||
import io.github.wulkanowy.data.onResourceData
|
||||
import io.github.wulkanowy.data.onResourceError
|
||||
import io.github.wulkanowy.data.onResourceIntermediate
|
||||
import io.github.wulkanowy.data.onResourceLoading
|
||||
import io.github.wulkanowy.data.onResourceNotLoading
|
||||
import io.github.wulkanowy.data.onResourceSuccess
|
||||
import io.github.wulkanowy.data.repositories.AttendanceRepository
|
||||
import io.github.wulkanowy.data.repositories.PreferencesRepository
|
||||
import io.github.wulkanowy.data.repositories.SemesterRepository
|
||||
import io.github.wulkanowy.data.repositories.StudentRepository
|
||||
import io.github.wulkanowy.data.resourceFlow
|
||||
import io.github.wulkanowy.ui.base.BasePresenter
|
||||
import io.github.wulkanowy.ui.base.ErrorHandler
|
||||
import io.github.wulkanowy.utils.*
|
||||
import io.github.wulkanowy.utils.AnalyticsHelper
|
||||
import io.github.wulkanowy.utils.capitalise
|
||||
import io.github.wulkanowy.utils.getLastSchoolDayIfHoliday
|
||||
import io.github.wulkanowy.utils.isExcusableOrNotExcused
|
||||
import io.github.wulkanowy.utils.isHolidays
|
||||
import io.github.wulkanowy.utils.monday
|
||||
import io.github.wulkanowy.utils.nextSchoolDay
|
||||
import io.github.wulkanowy.utils.previousOrSameSchoolDay
|
||||
import io.github.wulkanowy.utils.previousSchoolDay
|
||||
import io.github.wulkanowy.utils.sunday
|
||||
import io.github.wulkanowy.utils.toFormattedString
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import timber.log.Timber
|
||||
@ -195,6 +215,11 @@ class AttendancePresenter @Inject constructor(
|
||||
return true
|
||||
}
|
||||
|
||||
fun onCalculatorSwitchSelected(): Boolean {
|
||||
view?.openCalculatorView()
|
||||
return true
|
||||
}
|
||||
|
||||
private fun loadData(forceRefresh: Boolean = false) {
|
||||
Timber.i("Loading attendance data started")
|
||||
|
||||
|
@ -56,6 +56,8 @@ interface AttendanceView : BaseView {
|
||||
|
||||
fun openSummaryView()
|
||||
|
||||
fun openCalculatorView()
|
||||
|
||||
fun startSendMessageIntent(date: LocalDate, numbers: String, reason: String)
|
||||
|
||||
fun startActionMode()
|
||||
|
@ -0,0 +1,67 @@
|
||||
package io.github.wulkanowy.ui.modules.attendance.calculator
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.github.wulkanowy.R
|
||||
import io.github.wulkanowy.data.pojos.AttendanceData
|
||||
import io.github.wulkanowy.databinding.ItemAttendanceCalculatorHeaderBinding
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class AttendanceCalculatorAdapter @Inject constructor() :
|
||||
RecyclerView.Adapter<AttendanceCalculatorAdapter.ViewHolder>() {
|
||||
|
||||
var items = emptyList<AttendanceData>()
|
||||
|
||||
override fun getItemCount() = items.size
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
|
||||
ItemAttendanceCalculatorHeaderBinding.inflate(
|
||||
LayoutInflater.from(parent.context), parent, false
|
||||
)
|
||||
)
|
||||
|
||||
override fun onBindViewHolder(parent: ViewHolder, position: Int) {
|
||||
val context = parent.binding.root.context
|
||||
val item = items[position]
|
||||
|
||||
with(parent.binding) {
|
||||
attendanceCalculatorPercentage.text = "${item.presencePercentage.roundToInt()}"
|
||||
|
||||
attendanceCalculatorSummaryBalance.text = when {
|
||||
item.lessonBalance > 0 -> {
|
||||
context.getString(
|
||||
R.string.attendance_calculator_summary_balance_positive,
|
||||
item.lessonBalance
|
||||
)
|
||||
}
|
||||
|
||||
item.lessonBalance < 0 -> {
|
||||
context.getString(
|
||||
R.string.attendance_calculator_summary_balance_negative,
|
||||
abs(item.lessonBalance)
|
||||
)
|
||||
}
|
||||
|
||||
else -> context.getString(R.string.attendance_calculator_summary_balance_neutral)
|
||||
}
|
||||
attendanceCalculatorWarning.isVisible = item.lessonBalance < 0
|
||||
attendanceCalculatorTitle.text = item.subjectName
|
||||
attendanceCalculatorSummaryValues.text = if (item.total == 0) {
|
||||
context.getString(R.string.attendance_calculator_summary_values_empty)
|
||||
} else {
|
||||
context.getString(
|
||||
R.string.attendance_calculator_summary_values,
|
||||
item.presences,
|
||||
item.total
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ViewHolder(val binding: ItemAttendanceCalculatorHeaderBinding) :
|
||||
RecyclerView.ViewHolder(binding.root)
|
||||
}
|
@ -0,0 +1,133 @@
|
||||
package io.github.wulkanowy.ui.modules.attendance.calculator
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import io.github.wulkanowy.R
|
||||
import io.github.wulkanowy.data.pojos.AttendanceData
|
||||
import io.github.wulkanowy.databinding.FragmentAttendanceCalculatorBinding
|
||||
import io.github.wulkanowy.ui.base.BaseFragment
|
||||
import io.github.wulkanowy.ui.modules.main.MainActivity
|
||||
import io.github.wulkanowy.ui.modules.main.MainView
|
||||
import io.github.wulkanowy.ui.modules.settings.appearance.AppearanceFragment
|
||||
import io.github.wulkanowy.ui.widgets.DividerItemDecoration
|
||||
import io.github.wulkanowy.utils.getThemeAttrColor
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class AttendanceCalculatorFragment :
|
||||
BaseFragment<FragmentAttendanceCalculatorBinding>(R.layout.fragment_attendance_calculator),
|
||||
AttendanceCalculatorView, MainView.TitledView {
|
||||
|
||||
@Inject
|
||||
lateinit var presenter: AttendanceCalculatorPresenter
|
||||
|
||||
@Inject
|
||||
lateinit var attendanceCalculatorAdapter: AttendanceCalculatorAdapter
|
||||
|
||||
override val titleStringId get() = R.string.attendance_title
|
||||
|
||||
companion object {
|
||||
fun newInstance() = AttendanceCalculatorFragment()
|
||||
}
|
||||
|
||||
override val isViewEmpty get() = attendanceCalculatorAdapter.items.isEmpty()
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
binding = FragmentAttendanceCalculatorBinding.bind(view)
|
||||
messageContainer = binding.attendanceCalculatorRecycler
|
||||
presenter.onAttachView(this)
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.action_menu_attendance_calculator, menu)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return if (item.itemId == R.id.attendance_calculator_menu_settings) presenter.onSettingsSelected()
|
||||
else false
|
||||
}
|
||||
|
||||
override fun openSettingsView() {
|
||||
(activity as? MainActivity)?.pushView(AppearanceFragment.withFocusedPreference(getString(R.string.pref_key_attendance_target)))
|
||||
}
|
||||
|
||||
override fun initView() {
|
||||
with(binding.attendanceCalculatorRecycler) {
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
adapter = attendanceCalculatorAdapter
|
||||
addItemDecoration(DividerItemDecoration(context))
|
||||
}
|
||||
|
||||
with(binding) {
|
||||
attendanceCalculatorSwipe.setOnRefreshListener(presenter::onSwipeRefresh)
|
||||
attendanceCalculatorSwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary))
|
||||
attendanceCalculatorSwipe.setProgressBackgroundColorSchemeColor(
|
||||
requireContext().getThemeAttrColor(
|
||||
R.attr.colorSwipeRefresh
|
||||
)
|
||||
)
|
||||
attendanceCalculatorErrorRetry.setOnClickListener { presenter.onRetry() }
|
||||
attendanceCalculatorErrorDetails.setOnClickListener { presenter.onDetailsClick() }
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateData(data: List<AttendanceData>) {
|
||||
with(attendanceCalculatorAdapter) {
|
||||
items = data
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
override fun clearView() {
|
||||
with(attendanceCalculatorAdapter) {
|
||||
items = emptyList()
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
override fun showEmpty(show: Boolean) {
|
||||
binding.attendanceCalculatorEmpty.isVisible = show
|
||||
}
|
||||
|
||||
override fun showErrorView(show: Boolean) {
|
||||
binding.attendanceCalculatorError.isVisible = show
|
||||
}
|
||||
|
||||
override fun setErrorDetails(message: String) {
|
||||
binding.attendanceCalculatorErrorMessage.text = message
|
||||
}
|
||||
|
||||
override fun showProgress(show: Boolean) {
|
||||
binding.attendanceCalculatorProgress.isVisible = show
|
||||
}
|
||||
|
||||
override fun enableSwipe(enable: Boolean) {
|
||||
binding.attendanceCalculatorSwipe.isEnabled = enable
|
||||
}
|
||||
|
||||
override fun showContent(show: Boolean) {
|
||||
binding.attendanceCalculatorRecycler.isVisible = show
|
||||
}
|
||||
|
||||
override fun showRefresh(show: Boolean) {
|
||||
binding.attendanceCalculatorSwipe.isRefreshing = show
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
presenter.onDetachView()
|
||||
super.onDestroyView()
|
||||
}
|
||||
}
|
@ -0,0 +1,94 @@
|
||||
package io.github.wulkanowy.ui.modules.attendance.calculator
|
||||
|
||||
import io.github.wulkanowy.data.flatResourceFlow
|
||||
import io.github.wulkanowy.data.logResourceStatus
|
||||
import io.github.wulkanowy.data.onResourceData
|
||||
import io.github.wulkanowy.data.onResourceError
|
||||
import io.github.wulkanowy.data.onResourceIntermediate
|
||||
import io.github.wulkanowy.data.onResourceNotLoading
|
||||
import io.github.wulkanowy.data.repositories.SemesterRepository
|
||||
import io.github.wulkanowy.data.repositories.StudentRepository
|
||||
import io.github.wulkanowy.domain.attendance.GetAttendanceCalculatorDataUseCase
|
||||
import io.github.wulkanowy.ui.base.BasePresenter
|
||||
import io.github.wulkanowy.ui.base.ErrorHandler
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
class AttendanceCalculatorPresenter @Inject constructor(
|
||||
errorHandler: ErrorHandler,
|
||||
studentRepository: StudentRepository,
|
||||
private val semesterRepository: SemesterRepository,
|
||||
private val getAttendanceCalculatorData: GetAttendanceCalculatorDataUseCase,
|
||||
) : BasePresenter<AttendanceCalculatorView>(errorHandler, studentRepository) {
|
||||
|
||||
private lateinit var lastError: Throwable
|
||||
|
||||
override fun onAttachView(view: AttendanceCalculatorView) {
|
||||
super.onAttachView(view)
|
||||
view.initView()
|
||||
Timber.i("Attendance calculator view was initialized")
|
||||
errorHandler.showErrorMessage = ::showErrorViewOnError
|
||||
loadData()
|
||||
}
|
||||
|
||||
fun onSwipeRefresh() {
|
||||
Timber.i("Force refreshing the attendance calculator")
|
||||
loadData(forceRefresh = true)
|
||||
}
|
||||
|
||||
fun onRetry() {
|
||||
view?.run {
|
||||
showErrorView(false)
|
||||
showProgress(true)
|
||||
}
|
||||
loadData()
|
||||
}
|
||||
|
||||
fun onDetailsClick() {
|
||||
view?.showErrorDetailsDialog(lastError)
|
||||
}
|
||||
|
||||
private fun loadData(forceRefresh: Boolean = false) {
|
||||
flatResourceFlow {
|
||||
val student = studentRepository.getCurrentStudent()
|
||||
val semester = semesterRepository.getCurrentSemester(student)
|
||||
getAttendanceCalculatorData(student, semester, forceRefresh)
|
||||
}
|
||||
.logResourceStatus("load attendance calculator")
|
||||
.onResourceData {
|
||||
view?.run {
|
||||
showProgress(false)
|
||||
showErrorView(false)
|
||||
showContent(it.isNotEmpty())
|
||||
showEmpty(it.isEmpty())
|
||||
updateData(it)
|
||||
}
|
||||
}
|
||||
.onResourceIntermediate { view?.showRefresh(true) }
|
||||
.onResourceNotLoading {
|
||||
view?.run {
|
||||
enableSwipe(true)
|
||||
showRefresh(false)
|
||||
showProgress(false)
|
||||
}
|
||||
}
|
||||
.onResourceError(errorHandler::dispatch)
|
||||
.launch()
|
||||
}
|
||||
|
||||
private fun showErrorViewOnError(message: String, error: Throwable) {
|
||||
view?.run {
|
||||
if (isViewEmpty) {
|
||||
lastError = error
|
||||
setErrorDetails(message)
|
||||
showErrorView(true)
|
||||
showEmpty(false)
|
||||
} else showError(message, error)
|
||||
}
|
||||
}
|
||||
|
||||
fun onSettingsSelected(): Boolean {
|
||||
view?.openSettingsView()
|
||||
return true
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
package io.github.wulkanowy.ui.modules.attendance.calculator
|
||||
|
||||
import io.github.wulkanowy.data.pojos.AttendanceData
|
||||
import io.github.wulkanowy.ui.base.BaseView
|
||||
|
||||
interface AttendanceCalculatorView : BaseView {
|
||||
|
||||
val isViewEmpty: Boolean
|
||||
|
||||
fun initView()
|
||||
|
||||
fun showRefresh(show: Boolean)
|
||||
|
||||
fun showContent(show: Boolean)
|
||||
|
||||
fun showProgress(show: Boolean)
|
||||
|
||||
fun enableSwipe(enable: Boolean)
|
||||
|
||||
fun showEmpty(show: Boolean)
|
||||
|
||||
fun showErrorView(show: Boolean)
|
||||
|
||||
fun setErrorDetails(message: String)
|
||||
|
||||
fun updateData(data: List<AttendanceData>)
|
||||
|
||||
fun clearView()
|
||||
|
||||
fun openSettingsView()
|
||||
}
|
@ -5,6 +5,7 @@ import io.github.wulkanowy.data.repositories.StudentRepository
|
||||
import io.github.wulkanowy.ui.base.BasePresenter
|
||||
import io.github.wulkanowy.ui.base.ErrorHandler
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
class AuthPresenter @Inject constructor(
|
||||
@ -26,8 +27,12 @@ class AuthPresenter @Inject constructor(
|
||||
|
||||
private fun loadName() {
|
||||
presenterScope.launch {
|
||||
runCatching { studentRepository.getCurrentStudent(false) }
|
||||
.onSuccess { view?.showDescriptionWithName(it.studentName) }
|
||||
runCatching {
|
||||
studentRepository.getCurrentStudent(false)
|
||||
.studentName
|
||||
.replace(" ", "\u00A0")
|
||||
}
|
||||
.onSuccess { view?.showDescriptionWithName(it) }
|
||||
.onFailure { errorHandler.dispatch(it) }
|
||||
}
|
||||
}
|
||||
@ -57,8 +62,9 @@ class AuthPresenter @Inject constructor(
|
||||
val semester = semesterRepository.getCurrentSemester(student)
|
||||
|
||||
val isSuccess = studentRepository.authorizePermission(student, semester, pesel)
|
||||
Timber.d("Auth succeed: $isSuccess")
|
||||
if (isSuccess) {
|
||||
studentRepository.refreshStudentName(student, semester)
|
||||
studentRepository.refreshStudentAfterAuthorize(student, semester)
|
||||
}
|
||||
isSuccess
|
||||
}
|
||||
@ -68,6 +74,7 @@ class AuthPresenter @Inject constructor(
|
||||
view?.showContent(true)
|
||||
}
|
||||
.onSuccess {
|
||||
Timber.d("Auth fully succeed: $it")
|
||||
if (it) {
|
||||
view?.showSuccess(true)
|
||||
view?.showContent(false)
|
||||
|
@ -10,8 +10,8 @@ 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
|
||||
@ -21,7 +21,7 @@ import javax.inject.Inject
|
||||
class CaptchaDialog : BaseDialogFragment<DialogCaptchaBinding>() {
|
||||
|
||||
@Inject
|
||||
lateinit var sdk: Sdk
|
||||
lateinit var wulkanowySdkFactory: WulkanowySdkFactory
|
||||
|
||||
@Inject
|
||||
lateinit var webkitCookieManagerProxy: WebkitCookieManagerProxy
|
||||
@ -59,7 +59,7 @@ class CaptchaDialog : BaseDialogFragment<DialogCaptchaBinding>() {
|
||||
webView = this
|
||||
with(settings) {
|
||||
javaScriptEnabled = true
|
||||
userAgentString = sdk.userAgent
|
||||
userAgentString = wulkanowySdkFactory.create().userAgent
|
||||
}
|
||||
|
||||
webViewClient = object : WebViewClient() {
|
||||
|
@ -26,5 +26,7 @@ private fun generateSummary(subject: String, predicted: String, final: String) =
|
||||
proposedPoints = "",
|
||||
finalPoints = "",
|
||||
pointsSum = "",
|
||||
average = .0
|
||||
average = .0,
|
||||
pointsSumAllYear = null,
|
||||
averageAllYear = null,
|
||||
)
|
||||
|
@ -19,6 +19,6 @@ val debugSchoolAnnouncementItems = listOf(
|
||||
private fun generateAnnouncement(subject: String, content: String) = SchoolAnnouncement(
|
||||
subject = subject,
|
||||
content = content,
|
||||
userLoginId = 0,
|
||||
studentId = 0,
|
||||
date = LocalDate.now()
|
||||
)
|
||||
|
@ -266,7 +266,9 @@ class GradeAverageProvider @Inject constructor(
|
||||
proposedPoints = "",
|
||||
finalPoints = "",
|
||||
pointsSum = "",
|
||||
average = .0
|
||||
pointsSumAllYear = null,
|
||||
average = .0,
|
||||
averageAllYear = null,
|
||||
)
|
||||
}
|
||||
|
||||
@ -294,13 +296,15 @@ class GradeAverageProvider @Inject constructor(
|
||||
proposedPoints = "",
|
||||
finalPoints = "",
|
||||
pointsSum = "",
|
||||
pointsSumAllYear = null,
|
||||
average = when {
|
||||
calcAverage -> details
|
||||
.updateModifiers(student, params)
|
||||
.calcAverage(isOptionalArithmeticAverage = params.isOptionalArithmeticAverage)
|
||||
|
||||
else -> .0
|
||||
}
|
||||
},
|
||||
averageAllYear = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,6 @@ import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.View.INVISIBLE
|
||||
import android.view.View.VISIBLE
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
@ -31,14 +30,6 @@ class GradeFragment : BaseFragment<FragmentGradeBinding>(R.layout.fragment_grade
|
||||
@Inject
|
||||
lateinit var presenter: GradePresenter
|
||||
|
||||
private val pagerAdapter by lazy {
|
||||
BaseFragmentPagerAdapter(
|
||||
fragmentManager = childFragmentManager,
|
||||
pagesCount = 3,
|
||||
lifecycle = lifecycle,
|
||||
)
|
||||
}
|
||||
|
||||
private var semesterSwitchMenu: MenuItem? = null
|
||||
|
||||
companion object {
|
||||
@ -52,6 +43,8 @@ class GradeFragment : BaseFragment<FragmentGradeBinding>(R.layout.fragment_grade
|
||||
|
||||
override val currentPageIndex get() = binding.gradeViewPager.currentItem
|
||||
|
||||
private var pagerAdapter: BaseFragmentPagerAdapter? = null
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@ -71,13 +64,26 @@ class GradeFragment : BaseFragment<FragmentGradeBinding>(R.layout.fragment_grade
|
||||
}
|
||||
|
||||
override fun initView() {
|
||||
with(binding) {
|
||||
gradeErrorRetry.setOnClickListener { presenter.onRetry() }
|
||||
gradeErrorDetails.setOnClickListener { presenter.onDetailsClick() }
|
||||
}
|
||||
}
|
||||
|
||||
override fun initTabs(pageCount: Int) {
|
||||
pagerAdapter = BaseFragmentPagerAdapter(
|
||||
lifecycle = lifecycle,
|
||||
pagesCount = pageCount,
|
||||
fragmentManager = childFragmentManager
|
||||
)
|
||||
|
||||
with(binding.gradeViewPager) {
|
||||
adapter = pagerAdapter
|
||||
offscreenPageLimit = 3
|
||||
setOnSelectPageListener(presenter::onPageSelected)
|
||||
}
|
||||
|
||||
with(pagerAdapter) {
|
||||
with(pagerAdapter!!) {
|
||||
containerId = binding.gradeViewPager.id
|
||||
titleFactory = {
|
||||
when (it) {
|
||||
@ -99,11 +105,6 @@ class GradeFragment : BaseFragment<FragmentGradeBinding>(R.layout.fragment_grade
|
||||
}
|
||||
|
||||
binding.gradeTabLayout.elevation = requireContext().dpToPx(4f)
|
||||
|
||||
with(binding) {
|
||||
gradeErrorRetry.setOnClickListener { presenter.onRetry() }
|
||||
gradeErrorDetails.setOnClickListener { presenter.onDetailsClick() }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
@ -169,19 +170,20 @@ class GradeFragment : BaseFragment<FragmentGradeBinding>(R.layout.fragment_grade
|
||||
}
|
||||
|
||||
override fun notifyChildLoadData(index: Int, semesterId: Int, forceRefresh: Boolean) {
|
||||
(pagerAdapter.getFragmentInstance(index) as? GradeView.GradeChildView)
|
||||
(pagerAdapter?.getFragmentInstance(index) as? GradeView.GradeChildView)
|
||||
?.onParentLoadData(semesterId, forceRefresh)
|
||||
}
|
||||
|
||||
override fun notifyChildParentReselected(index: Int) {
|
||||
(pagerAdapter.getFragmentInstance(index) as? GradeView.GradeChildView)?.onParentReselected()
|
||||
(pagerAdapter?.getFragmentInstance(index) as? GradeView.GradeChildView)?.onParentReselected()
|
||||
}
|
||||
|
||||
override fun notifyChildSemesterChange(index: Int) {
|
||||
(pagerAdapter.getFragmentInstance(index) as? GradeView.GradeChildView)?.onParentChangeSemester()
|
||||
(pagerAdapter?.getFragmentInstance(index) as? GradeView.GradeChildView)?.onParentChangeSemester()
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
pagerAdapter = null
|
||||
presenter.onDetachView()
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
@ -22,11 +22,8 @@ class GradePresenter @Inject constructor(
|
||||
) : BasePresenter<GradeView>(errorHandler, studentRepository) {
|
||||
|
||||
private var selectedIndex = 0
|
||||
|
||||
private var schoolYear = 0
|
||||
|
||||
private var semesters = emptyList<Semester>()
|
||||
|
||||
private var availableSemesters = emptyList<Semester>()
|
||||
private val loadedSemesterId = mutableMapOf<Int, Int>()
|
||||
|
||||
private lateinit var lastError: Throwable
|
||||
@ -40,7 +37,7 @@ class GradePresenter @Inject constructor(
|
||||
}
|
||||
|
||||
fun onCreateMenu() {
|
||||
if (semesters.isEmpty()) view?.showSemesterSwitch(false)
|
||||
if (availableSemesters.isEmpty()) view?.showSemesterSwitch(false)
|
||||
}
|
||||
|
||||
fun onViewReselected() {
|
||||
@ -49,8 +46,8 @@ class GradePresenter @Inject constructor(
|
||||
}
|
||||
|
||||
fun onSemesterSwitch(): Boolean {
|
||||
if (semesters.isNotEmpty()) {
|
||||
view?.showSemesterDialog(selectedIndex - 1, semesters.take(2))
|
||||
if (availableSemesters.isNotEmpty()) {
|
||||
view?.showSemesterDialog(selectedIndex - 1, availableSemesters.take(2))
|
||||
}
|
||||
return true
|
||||
}
|
||||
@ -83,7 +80,7 @@ class GradePresenter @Inject constructor(
|
||||
}
|
||||
|
||||
fun onPageSelected(index: Int) {
|
||||
if (semesters.isNotEmpty()) loadChild(index)
|
||||
if (availableSemesters.isNotEmpty()) loadChild(index)
|
||||
}
|
||||
|
||||
fun onRetry() {
|
||||
@ -101,16 +98,24 @@ class GradePresenter @Inject constructor(
|
||||
private fun loadData() {
|
||||
resourceFlow {
|
||||
val student = studentRepository.getCurrentStudent()
|
||||
semesterRepository.getSemesters(student, refreshOnNoCurrent = true)
|
||||
val semesters = semesterRepository.getSemesters(student, refreshOnNoCurrent = true)
|
||||
|
||||
student to semesters
|
||||
}
|
||||
.logResourceStatus("load grade data")
|
||||
.onResourceData {
|
||||
val current = it.getCurrentOrLast()
|
||||
selectedIndex = if (selectedIndex == 0) current.semesterName else selectedIndex
|
||||
schoolYear = current.schoolYear
|
||||
semesters = it.filter { semester -> semester.diaryId == current.diaryId }
|
||||
view?.setCurrentSemesterName(current.semesterName, schoolYear)
|
||||
.onResourceData { (student, semesters) ->
|
||||
val currentSemester = semesters.getCurrentOrLast()
|
||||
selectedIndex =
|
||||
if (selectedIndex == 0) currentSemester.semesterName else selectedIndex
|
||||
schoolYear = currentSemester.schoolYear
|
||||
availableSemesters = semesters.filter { semester ->
|
||||
semester.diaryId == currentSemester.diaryId
|
||||
}
|
||||
|
||||
view?.run {
|
||||
initTabs(if (student.isEduOne == true) 2 else 3)
|
||||
setCurrentSemesterName(currentSemester.semesterName, schoolYear)
|
||||
|
||||
Timber.i("Loading grade data: Attempt load index $currentPageIndex")
|
||||
loadChild(currentPageIndex)
|
||||
showErrorView(false)
|
||||
@ -131,10 +136,10 @@ class GradePresenter @Inject constructor(
|
||||
}
|
||||
|
||||
private fun loadChild(index: Int, forceRefresh: Boolean = false) {
|
||||
Timber.d("Load grade tab child. Selected semester: $selectedIndex, semesters: ${semesters.joinToString { it.semesterName.toString() }}")
|
||||
Timber.d("Load grade tab child. Selected semester: $selectedIndex, semesters: ${availableSemesters.joinToString { it.semesterName.toString() }}")
|
||||
|
||||
val newSelectedSemesterId = try {
|
||||
semesters.first { it.semesterName == selectedIndex }.semesterId
|
||||
availableSemesters.first { it.semesterName == selectedIndex }.semesterId
|
||||
} catch (e: NoSuchElementException) {
|
||||
Timber.e(e, "Selected semester no exists")
|
||||
return
|
||||
|
@ -9,6 +9,8 @@ interface GradeView : BaseView {
|
||||
|
||||
fun initView()
|
||||
|
||||
fun initTabs(pageCount: Int)
|
||||
|
||||
fun showContent(show: Boolean)
|
||||
|
||||
fun showProgress(show: Boolean)
|
||||
|
@ -96,9 +96,11 @@ class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter<Recycler
|
||||
ViewType.HEADER.id -> HeaderViewHolder(
|
||||
HeaderGradeDetailsBinding.inflate(inflater, parent, false)
|
||||
)
|
||||
|
||||
ViewType.ITEM.id -> ItemViewHolder(
|
||||
ItemGradeDetailsBinding.inflate(inflater, parent, false)
|
||||
)
|
||||
|
||||
else -> throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
@ -110,6 +112,7 @@ class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter<Recycler
|
||||
header = items[position].value as GradeDetailsHeader,
|
||||
position = position
|
||||
)
|
||||
|
||||
is ItemViewHolder -> bindItemViewHolder(
|
||||
holder = holder,
|
||||
grade = items[position].value as Grade
|
||||
@ -133,6 +136,10 @@ class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter<Recycler
|
||||
maxLines = if (expandedPositions[headerPosition]) 2 else 1
|
||||
}
|
||||
gradeHeaderAverage.text = formatAverage(header.average, root.context.resources)
|
||||
with(gradeHeaderAverageAllYear) {
|
||||
isVisible = header.averageAllYear != null && header.averageAllYear != .0
|
||||
text = formatAverageAllYear(header.averageAllYear, root.context.resources)
|
||||
}
|
||||
gradeHeaderPointsSum.text =
|
||||
context.getString(R.string.grade_points_sum, header.pointsSum)
|
||||
gradeHeaderPointsSum.isVisible = !header.pointsSum.isNullOrEmpty()
|
||||
@ -233,6 +240,13 @@ class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter<Recycler
|
||||
resources.getString(R.string.grade_average, average)
|
||||
}
|
||||
|
||||
private fun formatAverageAllYear(average: Double?, resources: Resources) =
|
||||
if (average == null || average == .0) {
|
||||
resources.getString(R.string.grade_no_average)
|
||||
} else {
|
||||
resources.getString(R.string.grade_average_year, average)
|
||||
}
|
||||
|
||||
private class HeaderViewHolder(val binding: HeaderGradeDetailsBinding) :
|
||||
RecyclerView.ViewHolder(binding.root)
|
||||
|
||||
|
@ -13,6 +13,7 @@ data class GradeDetailsItem(
|
||||
data class GradeDetailsHeader(
|
||||
val subject: String,
|
||||
val average: Double?,
|
||||
val averageAllYear: Double?,
|
||||
val pointsSum: String?,
|
||||
val grades: List<GradeDetailsItem>
|
||||
) {
|
||||
|
@ -226,8 +226,9 @@ class GradeDetailsPresenter @Inject constructor(
|
||||
GradeDetailsHeader(
|
||||
subject = gradeSubject.subject,
|
||||
average = gradeSubject.average,
|
||||
averageAllYear = gradeSubject.summary.averageAllYear,
|
||||
pointsSum = gradeSubject.points,
|
||||
grades = subItems
|
||||
grades = subItems,
|
||||
).apply {
|
||||
newGrades = gradeSubject.grades.filter { grade -> !grade.isRead }.size
|
||||
}, ViewType.HEADER
|
||||
|
@ -3,6 +3,7 @@ package io.github.wulkanowy.ui.modules.grade.summary
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.github.wulkanowy.R
|
||||
@ -65,37 +66,55 @@ class GradeSummaryAdapter @Inject constructor(
|
||||
val gradeSummaries = items
|
||||
.filter { it.gradeDescriptive == null }
|
||||
.map { it.gradeSummary }
|
||||
val isSecondSemester = items.any { item ->
|
||||
item.gradeSummary.let { it.averageAllYear != null && it.averageAllYear != .0 }
|
||||
}
|
||||
|
||||
val context = binding.root.context
|
||||
val finalItemsCount = gradeSummaries.count { isGradeValid(it.finalGrade) }
|
||||
val calculatedItemsCount = gradeSummaries.count { value -> value.average != 0.0 }
|
||||
val calculatedSemesterItemsCount = gradeSummaries.count { value -> value.average != 0.0 }
|
||||
val calculatedAnnualItemsCount =
|
||||
gradeSummaries.count { value -> value.averageAllYear != 0.0 }
|
||||
val allItemsCount = gradeSummaries.count { !it.subject.equals("zachowanie", true) }
|
||||
val finalAverage = gradeSummaries.calcFinalAverage(
|
||||
preferencesRepository.gradePlusModifier,
|
||||
preferencesRepository.gradeMinusModifier
|
||||
plusModifier = preferencesRepository.gradePlusModifier,
|
||||
minusModifier = preferencesRepository.gradeMinusModifier,
|
||||
)
|
||||
val calculatedAverage = gradeSummaries.filter { value -> value.average != 0.0 }
|
||||
val calculatedSemesterAverage = gradeSummaries.filter { value -> value.average != 0.0 }
|
||||
.map { values -> values.average }
|
||||
.reversed() // fix average precision
|
||||
.average()
|
||||
.let { if (it.isNaN()) 0.0 else it }
|
||||
val calculatedAnnualAverage = gradeSummaries.filter { value -> value.averageAllYear != 0.0 }
|
||||
.mapNotNull { values -> values.averageAllYear }
|
||||
.reversed() // fix average precision
|
||||
.average()
|
||||
.let { if (it.isNaN()) 0.0 else it }
|
||||
|
||||
with(binding) {
|
||||
gradeSummaryScrollableHeaderCalculated.text = formatAverage(calculatedSemesterAverage)
|
||||
gradeSummaryScrollableHeaderCalculatedAnnual.text =
|
||||
formatAverage(calculatedAnnualAverage)
|
||||
gradeSummaryScrollableHeaderFinal.text = formatAverage(finalAverage)
|
||||
gradeSummaryScrollableHeaderCalculated.text = formatAverage(calculatedAverage)
|
||||
gradeSummaryScrollableHeaderFinalSubjectCount.text =
|
||||
context.getString(
|
||||
R.string.grade_summary_from_subjects,
|
||||
finalItemsCount,
|
||||
allItemsCount
|
||||
)
|
||||
gradeSummaryScrollableHeaderCalculatedSubjectCount.text = context.getString(
|
||||
gradeSummaryScrollableHeaderFinalSubjectCount.text = context.getString(
|
||||
R.string.grade_summary_from_subjects,
|
||||
calculatedItemsCount,
|
||||
finalItemsCount,
|
||||
allItemsCount
|
||||
)
|
||||
gradeSummaryScrollableHeaderCalculatedSubjectCount.text = context.getString(
|
||||
R.string.grade_summary_from_subjects,
|
||||
calculatedSemesterItemsCount,
|
||||
allItemsCount
|
||||
)
|
||||
gradeSummaryScrollableHeaderCalculatedSubjectCountAnnual.text = context.getString(
|
||||
R.string.grade_summary_from_subjects,
|
||||
calculatedAnnualItemsCount,
|
||||
allItemsCount
|
||||
)
|
||||
gradeSummaryScrollableHeaderCalculatedAnnualContainer.isVisible = isSecondSemester
|
||||
|
||||
gradeSummaryCalculatedAverageHelp.setOnClickListener { onCalculatedHelpClickListener() }
|
||||
gradeSummaryCalculatedAverageHelpAnnual.setOnClickListener { onCalculatedHelpClickListener() }
|
||||
gradeSummaryFinalAverageHelp.setOnClickListener { onFinalHelpClickListener() }
|
||||
}
|
||||
}
|
||||
@ -107,7 +126,12 @@ class GradeSummaryAdapter @Inject constructor(
|
||||
with(binding) {
|
||||
gradeSummaryItemTitle.text = gradeSummary.subject
|
||||
gradeSummaryItemPoints.text = gradeSummary.pointsSum
|
||||
|
||||
gradeSummaryItemAverage.text = formatAverage(gradeSummary.average, "")
|
||||
gradeSummaryItemAverageAllYear.text = gradeSummary.averageAllYear?.let {
|
||||
formatAverage(it, "")
|
||||
}
|
||||
|
||||
gradeSummaryItemPredicted.text =
|
||||
"${gradeSummary.predictedGrade} ${gradeSummary.proposedPoints}".trim()
|
||||
gradeSummaryItemFinal.text =
|
||||
@ -116,6 +140,12 @@ class GradeSummaryAdapter @Inject constructor(
|
||||
root.context.getString(R.string.all_no_data)
|
||||
}
|
||||
|
||||
gradeSummaryItemAverageContainer.isVisible = gradeSummary.average != .0
|
||||
gradeSummaryItemAverageDivider.isVisible = gradeSummaryItemAverageContainer.isVisible
|
||||
gradeSummaryItemAverageAllYearContainer.isGone =
|
||||
gradeSummary.averageAllYear == null || gradeSummary.averageAllYear == .0
|
||||
gradeSummaryItemAverageAllYearDivider.isGone =
|
||||
gradeSummaryItemAverageAllYearContainer.isGone
|
||||
gradeSummaryItemFinalDivider.isVisible = gradeDescriptive == null
|
||||
gradeSummaryItemPredictedDivider.isVisible = gradeDescriptive == null
|
||||
gradeSummaryItemPointsDivider.isVisible = gradeDescriptive == null
|
||||
@ -123,6 +153,7 @@ class GradeSummaryAdapter @Inject constructor(
|
||||
gradeSummaryItemFinalContainer.isVisible = gradeDescriptive == null
|
||||
gradeSummaryItemDescriptiveContainer.isVisible = gradeDescriptive != null
|
||||
gradeSummaryItemPointsContainer.isVisible = gradeSummary.pointsSum.isNotBlank()
|
||||
gradeSummaryItemPointsDivider.isVisible = gradeSummaryItemPointsContainer.isVisible
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -98,7 +98,9 @@ class HomeworkAddDialog : BaseDialogFragment<DialogHomeworkAddBinding>(), Homewo
|
||||
rangeEnd = LocalDate.now().lastSchoolDayInSchoolYear,
|
||||
onDateSelected = {
|
||||
date = it
|
||||
binding.homeworkDialogDate.editText?.setText(date!!.toFormattedString())
|
||||
if (isAdded) {
|
||||
binding.homeworkDialogDate.editText?.setText(date!!.toFormattedString())
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -19,19 +19,23 @@ class LoginStudentSelectAdapter @Inject constructor() :
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
return when (LoginStudentSelectItemType.values()[viewType]) {
|
||||
return when (LoginStudentSelectItemType.entries[viewType]) {
|
||||
LoginStudentSelectItemType.EMPTY_SYMBOLS_HEADER -> EmptySymbolsHeaderViewHolder(
|
||||
ItemLoginStudentSelectEmptySymbolHeaderBinding.inflate(inflater, parent, false),
|
||||
)
|
||||
|
||||
LoginStudentSelectItemType.SYMBOL_HEADER -> SymbolsHeaderViewHolder(
|
||||
ItemLoginStudentSelectHeaderSymbolBinding.inflate(inflater, parent, false)
|
||||
)
|
||||
|
||||
LoginStudentSelectItemType.SCHOOL_HEADER -> SchoolHeaderViewHolder(
|
||||
ItemLoginStudentSelectHeaderSchoolBinding.inflate(inflater, parent, false)
|
||||
)
|
||||
|
||||
LoginStudentSelectItemType.STUDENT -> StudentViewHolder(
|
||||
ItemLoginStudentSelectStudentBinding.inflate(inflater, parent, false)
|
||||
)
|
||||
|
||||
LoginStudentSelectItemType.HELP -> HelpViewHolder(
|
||||
ItemLoginStudentSelectHelpBinding.inflate(inflater, parent, false)
|
||||
)
|
||||
@ -98,9 +102,11 @@ class LoginStudentSelectAdapter @Inject constructor() :
|
||||
with(binding) {
|
||||
loginStudentSelectHeaderSchoolName.text = buildString {
|
||||
append(item.unit.schoolName.trim())
|
||||
append(" (")
|
||||
append(item.unit.schoolShortName)
|
||||
append(")")
|
||||
if (item.unit.schoolShortName.isNotBlank()) {
|
||||
append(" (")
|
||||
append(item.unit.schoolShortName)
|
||||
append(")")
|
||||
}
|
||||
}
|
||||
loginStudentSelectHeaderSchoolDetails.isVisible = item.unit.students.isEmpty()
|
||||
loginStudentSelectHeaderSchoolError.text = item.unit.error?.message
|
||||
@ -170,9 +176,11 @@ class LoginStudentSelectAdapter @Inject constructor() :
|
||||
oldItem is LoginStudentSelectItem.SymbolHeader && newItem is LoginStudentSelectItem.SymbolHeader -> {
|
||||
oldItem.symbol == newItem.symbol
|
||||
}
|
||||
|
||||
oldItem is LoginStudentSelectItem.Student && newItem is LoginStudentSelectItem.Student -> {
|
||||
oldItem.student == newItem.student
|
||||
}
|
||||
|
||||
else -> oldItem == newItem
|
||||
}
|
||||
|
||||
|
@ -6,10 +6,12 @@ import androidx.core.os.bundleOf
|
||||
import androidx.core.view.isVisible
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import io.github.wulkanowy.R
|
||||
import io.github.wulkanowy.data.db.entities.AdminMessage
|
||||
import io.github.wulkanowy.data.pojos.RegisterUser
|
||||
import io.github.wulkanowy.data.repositories.PreferencesRepository
|
||||
import io.github.wulkanowy.databinding.FragmentLoginStudentSelectBinding
|
||||
import io.github.wulkanowy.ui.base.BaseFragment
|
||||
import io.github.wulkanowy.ui.modules.dashboard.viewholders.AdminMessageViewHolder
|
||||
import io.github.wulkanowy.ui.modules.login.LoginActivity
|
||||
import io.github.wulkanowy.ui.modules.login.LoginData
|
||||
import io.github.wulkanowy.ui.modules.login.support.LoginSupportDialog
|
||||
@ -111,6 +113,19 @@ class LoginStudentSelectFragment :
|
||||
LoginSupportDialog.newInstance(supportInfo).show(childFragmentManager, "support_dialog")
|
||||
}
|
||||
|
||||
override fun showAdminMessage(adminMessage: AdminMessage?) {
|
||||
AdminMessageViewHolder(
|
||||
binding = binding.loginStudentSelectAdminMessage,
|
||||
onAdminMessageDismissClickListener = presenter::onAdminMessageDismissed,
|
||||
onAdminMessageClickListener = presenter::onAdminMessageSelected,
|
||||
).bind(adminMessage)
|
||||
binding.loginStudentSelectAdminMessage.root.isVisible = adminMessage != null
|
||||
}
|
||||
|
||||
override fun openInternetBrowser(url: String) {
|
||||
requireContext().openInternetBrowser(url)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
presenter.onDetachView()
|
||||
super.onDestroyView()
|
||||
|
@ -2,16 +2,24 @@ package io.github.wulkanowy.ui.modules.login.studentselect
|
||||
|
||||
import io.github.wulkanowy.data.Resource
|
||||
import io.github.wulkanowy.data.dataOrNull
|
||||
import io.github.wulkanowy.data.db.entities.AdminMessage
|
||||
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
|
||||
import io.github.wulkanowy.data.enums.MessageType
|
||||
import io.github.wulkanowy.data.flatResourceFlow
|
||||
import io.github.wulkanowy.data.logResourceStatus
|
||||
import io.github.wulkanowy.data.mappers.mapToStudentWithSemesters
|
||||
import io.github.wulkanowy.data.onResourceData
|
||||
import io.github.wulkanowy.data.onResourceError
|
||||
import io.github.wulkanowy.data.pojos.RegisterStudent
|
||||
import io.github.wulkanowy.data.pojos.RegisterSymbol
|
||||
import io.github.wulkanowy.data.pojos.RegisterUnit
|
||||
import io.github.wulkanowy.data.pojos.RegisterUser
|
||||
import io.github.wulkanowy.data.repositories.PreferencesRepository
|
||||
import io.github.wulkanowy.data.repositories.SchoolsRepository
|
||||
import io.github.wulkanowy.data.repositories.StudentRepository
|
||||
import io.github.wulkanowy.data.resourceFlow
|
||||
import io.github.wulkanowy.domain.adminmessage.GetAppropriateAdminMessageUseCase
|
||||
import io.github.wulkanowy.sdk.scrapper.exception.StudentGraduateException
|
||||
import io.github.wulkanowy.sdk.scrapper.login.InvalidSymbolException
|
||||
import io.github.wulkanowy.services.sync.SyncManager
|
||||
import io.github.wulkanowy.ui.base.BasePresenter
|
||||
@ -32,6 +40,8 @@ class LoginStudentSelectPresenter @Inject constructor(
|
||||
private val syncManager: SyncManager,
|
||||
private val analytics: AnalyticsHelper,
|
||||
private val appInfo: AppInfo,
|
||||
private val preferencesRepository: PreferencesRepository,
|
||||
private val getAppropriateAdminMessageUseCase: GetAppropriateAdminMessageUseCase
|
||||
) : BasePresenter<LoginStudentSelectView>(loginErrorHandler, studentRepository) {
|
||||
|
||||
private var lastError: Throwable? = null
|
||||
@ -64,6 +74,7 @@ class LoginStudentSelectPresenter @Inject constructor(
|
||||
this.loginData = loginData
|
||||
this.registerUser = registerUser
|
||||
loadData()
|
||||
loadAdminMessage()
|
||||
}
|
||||
|
||||
private fun loadData() {
|
||||
@ -87,7 +98,20 @@ class LoginStudentSelectPresenter @Inject constructor(
|
||||
refreshItems()
|
||||
}
|
||||
}
|
||||
}.launch()
|
||||
}.launch("load_data")
|
||||
}
|
||||
|
||||
private fun loadAdminMessage() {
|
||||
flatResourceFlow {
|
||||
getAppropriateAdminMessageUseCase(
|
||||
scrapperBaseUrl = registerUser.scrapperBaseUrl.orEmpty(),
|
||||
type = MessageType.LOGIN_STUDENT_SELECT_MESSAGE,
|
||||
)
|
||||
}
|
||||
.logResourceStatus("load login admin message")
|
||||
.onResourceData { view?.showAdminMessage(it) }
|
||||
.onResourceError { view?.showAdminMessage(null) }
|
||||
.launch("load_admin_message")
|
||||
}
|
||||
|
||||
private fun getStudentsWithCurrentlyActiveSemesters(): List<LoginStudentSelectItem.Student> {
|
||||
@ -108,8 +132,8 @@ class LoginStudentSelectPresenter @Inject constructor(
|
||||
}
|
||||
|
||||
private fun createItems(): List<LoginStudentSelectItem> = buildList {
|
||||
val notEmptySymbols = registerUser.symbols.filter { it.schools.isNotEmpty() }
|
||||
val emptySymbols = registerUser.symbols.filter { it.schools.isEmpty() }
|
||||
val notEmptySymbols = registerUser.symbols.filter { it.shouldShowOnTop() }
|
||||
val emptySymbols = registerUser.symbols.filter { !it.shouldShowOnTop() }
|
||||
|
||||
if (emptySymbols.isNotEmpty() && notEmptySymbols.isNotEmpty() && emptySymbols.any { it.symbol == loginData.userEnteredSymbol }) {
|
||||
add(createEmptySymbolItem(emptySymbols.first { it.symbol == loginData.userEnteredSymbol }))
|
||||
@ -127,6 +151,10 @@ class LoginStudentSelectPresenter @Inject constructor(
|
||||
add(helpItem)
|
||||
}
|
||||
|
||||
private fun RegisterSymbol.shouldShowOnTop(): Boolean {
|
||||
return schools.isNotEmpty() || error is StudentGraduateException
|
||||
}
|
||||
|
||||
private fun createNotEmptySymbolItems(
|
||||
notEmptySymbols: List<RegisterSymbol>,
|
||||
students: List<StudentWithSemesters>,
|
||||
@ -336,4 +364,14 @@ class LoginStudentSelectPresenter @Inject constructor(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onAdminMessageSelected(url: String?) {
|
||||
url?.let { view?.openInternetBrowser(it) }
|
||||
}
|
||||
|
||||
fun onAdminMessageDismissed(adminMessage: AdminMessage) {
|
||||
preferencesRepository.dismissedAdminMessageIds += adminMessage.id
|
||||
|
||||
view?.showAdminMessage(null)
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
package io.github.wulkanowy.ui.modules.login.studentselect
|
||||
|
||||
import io.github.wulkanowy.data.db.entities.AdminMessage
|
||||
import io.github.wulkanowy.ui.base.BaseView
|
||||
import io.github.wulkanowy.ui.modules.login.LoginData
|
||||
import io.github.wulkanowy.ui.modules.login.support.LoginSupportInfo
|
||||
@ -25,4 +26,8 @@ interface LoginStudentSelectView : BaseView {
|
||||
fun openDiscordInvite()
|
||||
|
||||
fun openEmail(supportInfo: LoginSupportInfo)
|
||||
|
||||
fun showAdminMessage(adminMessage: AdminMessage?)
|
||||
|
||||
fun openInternetBrowser(url: String)
|
||||
}
|
||||
|
@ -9,13 +9,16 @@ import android.view.inputmethod.EditorInfo.IME_NULL
|
||||
import android.widget.ArrayAdapter
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.core.text.parseAsHtml
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.widget.doOnTextChanged
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import io.github.wulkanowy.R
|
||||
import io.github.wulkanowy.data.db.entities.AdminMessage
|
||||
import io.github.wulkanowy.data.pojos.RegisterUser
|
||||
import io.github.wulkanowy.data.repositories.PreferencesRepository
|
||||
import io.github.wulkanowy.databinding.FragmentLoginSymbolBinding
|
||||
import io.github.wulkanowy.ui.base.BaseFragment
|
||||
import io.github.wulkanowy.ui.modules.dashboard.viewholders.AdminMessageViewHolder
|
||||
import io.github.wulkanowy.ui.modules.login.LoginActivity
|
||||
import io.github.wulkanowy.ui.modules.login.LoginData
|
||||
import io.github.wulkanowy.ui.modules.login.support.LoginSupportDialog
|
||||
@ -179,4 +182,17 @@ class LoginSymbolFragment :
|
||||
override fun openSupportDialog(supportInfo: LoginSupportInfo) {
|
||||
LoginSupportDialog.newInstance(supportInfo).show(childFragmentManager, "support_dialog")
|
||||
}
|
||||
|
||||
override fun showAdminMessage(adminMessage: AdminMessage?) {
|
||||
AdminMessageViewHolder(
|
||||
binding = binding.loginSymbolAdminMessage,
|
||||
onAdminMessageDismissClickListener = presenter::onAdminMessageDismissed,
|
||||
onAdminMessageClickListener = presenter::onAdminMessageSelected,
|
||||
).bind(adminMessage)
|
||||
binding.loginSymbolAdminMessage.root.isVisible = adminMessage != null
|
||||
}
|
||||
|
||||
override fun openInternetBrowser(url: String) {
|
||||
requireContext().openInternetBrowser(url)
|
||||
}
|
||||
}
|
||||
|
@ -2,10 +2,18 @@ package io.github.wulkanowy.ui.modules.login.symbol
|
||||
|
||||
import io.github.wulkanowy.data.Resource
|
||||
import io.github.wulkanowy.data.dataOrNull
|
||||
import io.github.wulkanowy.data.db.entities.AdminMessage
|
||||
import io.github.wulkanowy.data.enums.MessageType
|
||||
import io.github.wulkanowy.data.flatResourceFlow
|
||||
import io.github.wulkanowy.data.logResourceStatus
|
||||
import io.github.wulkanowy.data.onResourceData
|
||||
import io.github.wulkanowy.data.onResourceError
|
||||
import io.github.wulkanowy.data.onResourceNotLoading
|
||||
import io.github.wulkanowy.data.pojos.RegisterUser
|
||||
import io.github.wulkanowy.data.repositories.PreferencesRepository
|
||||
import io.github.wulkanowy.data.repositories.StudentRepository
|
||||
import io.github.wulkanowy.data.resourceFlow
|
||||
import io.github.wulkanowy.domain.adminmessage.GetAppropriateAdminMessageUseCase
|
||||
import io.github.wulkanowy.sdk.scrapper.getNormalizedSymbol
|
||||
import io.github.wulkanowy.sdk.scrapper.login.InvalidSymbolException
|
||||
import io.github.wulkanowy.ui.base.BasePresenter
|
||||
@ -21,7 +29,9 @@ import javax.inject.Inject
|
||||
class LoginSymbolPresenter @Inject constructor(
|
||||
studentRepository: StudentRepository,
|
||||
private val loginErrorHandler: LoginErrorHandler,
|
||||
private val analytics: AnalyticsHelper
|
||||
private val analytics: AnalyticsHelper,
|
||||
private val preferencesRepository: PreferencesRepository,
|
||||
private val getAppropriateAdminMessageUseCase: GetAppropriateAdminMessageUseCase,
|
||||
) : BasePresenter<LoginSymbolView>(loginErrorHandler, studentRepository) {
|
||||
|
||||
private var lastError: Throwable? = null
|
||||
@ -43,6 +53,21 @@ class LoginSymbolPresenter @Inject constructor(
|
||||
clearAndFocusSymbol()
|
||||
showSoftKeyboard()
|
||||
}
|
||||
|
||||
loadAdminMessage()
|
||||
}
|
||||
|
||||
private fun loadAdminMessage() {
|
||||
flatResourceFlow {
|
||||
getAppropriateAdminMessageUseCase(
|
||||
scrapperBaseUrl = loginData.baseUrl,
|
||||
type = MessageType.LOGIN_SYMBOL_MESSAGE,
|
||||
)
|
||||
}
|
||||
.logResourceStatus("load login admin message")
|
||||
.onResourceData { view?.showAdminMessage(it) }
|
||||
.onResourceError { view?.showAdminMessage(null) }
|
||||
.launch("load_admin_message")
|
||||
}
|
||||
|
||||
fun onSymbolTextChanged() {
|
||||
@ -166,4 +191,14 @@ class LoginSymbolPresenter @Inject constructor(
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun onAdminMessageSelected(url: String?) {
|
||||
url?.let { view?.openInternetBrowser(it) }
|
||||
}
|
||||
|
||||
fun onAdminMessageDismissed(adminMessage: AdminMessage) {
|
||||
preferencesRepository.dismissedAdminMessageIds += adminMessage.id
|
||||
|
||||
view?.showAdminMessage(null)
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
package io.github.wulkanowy.ui.modules.login.symbol
|
||||
|
||||
import io.github.wulkanowy.data.db.entities.AdminMessage
|
||||
import io.github.wulkanowy.data.pojos.RegisterUser
|
||||
import io.github.wulkanowy.ui.base.BaseView
|
||||
import io.github.wulkanowy.ui.modules.login.LoginData
|
||||
@ -44,4 +45,8 @@ interface LoginSymbolView : BaseView {
|
||||
fun openFaqPage()
|
||||
|
||||
fun openSupportDialog(supportInfo: LoginSupportInfo)
|
||||
|
||||
fun showAdminMessage(adminMessage: AdminMessage?)
|
||||
|
||||
fun openInternetBrowser(url: String)
|
||||
}
|
||||
|
@ -33,4 +33,4 @@ class LuckyNumberHistoryAdapter @Inject constructor() :
|
||||
}
|
||||
|
||||
class ItemViewHolder(val binding: ItemLuckyNumberHistoryBinding) : RecyclerView.ViewHolder(binding.root)
|
||||
}
|
||||
}
|
||||
|
@ -145,7 +145,11 @@ class LuckyNumberWidgetProvider : AppWidgetProvider() {
|
||||
}
|
||||
|
||||
if (currentStudent != null) {
|
||||
luckyNumberRepository.getLuckyNumber(currentStudent, forceRefresh = false)
|
||||
luckyNumberRepository.getLuckyNumber(
|
||||
student = currentStudent,
|
||||
forceRefresh = false,
|
||||
isFromAppWidget = true
|
||||
)
|
||||
.toFirstResult()
|
||||
.dataOrThrow
|
||||
} else null
|
||||
|
@ -73,7 +73,7 @@ class MainPresenter @Inject constructor(
|
||||
syncManager.startPeriodicSyncWorker()
|
||||
|
||||
checkAppSupport()
|
||||
checkCurrentStudentAuthorizationStatus()
|
||||
updateCurrentStudentAuthStatus()
|
||||
|
||||
analytics.logEvent("app_open", "destination" to initDestination.toString())
|
||||
Timber.i("Main view was initialized with $initDestination")
|
||||
@ -193,12 +193,10 @@ class MainPresenter @Inject constructor(
|
||||
view?.showStudentAvatar(currentStudent)
|
||||
}
|
||||
|
||||
private fun checkCurrentStudentAuthorizationStatus() {
|
||||
private fun updateCurrentStudentAuthStatus() {
|
||||
presenterScope.launch {
|
||||
runCatching { studentRepository.checkCurrentStudentAuthorizationStatus() }
|
||||
runCatching { studentRepository.updateCurrentStudentAuthStatus() }
|
||||
.onFailure { errorHandler.dispatch(it) }
|
||||
|
||||
Timber.i("Current student authorization status checked")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,9 @@ package io.github.wulkanowy.ui.modules.settings.appearance
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.SeekBarPreference
|
||||
import com.yariksoffice.lingver.Lingver
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import io.github.wulkanowy.R
|
||||
@ -29,13 +31,31 @@ class AppearanceFragment : PreferenceFragmentCompat(),
|
||||
|
||||
override val titleStringId get() = R.string.pref_settings_appearance_title
|
||||
|
||||
companion object {
|
||||
fun withFocusedPreference(key: String) = AppearanceFragment().apply {
|
||||
arguments = bundleOf(FOCUSED_KEY to key)
|
||||
}
|
||||
|
||||
private const val FOCUSED_KEY = "focusedKey"
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
presenter.onAttachView(this)
|
||||
arguments?.getString(FOCUSED_KEY)?.let { scrollToPreference(it) }
|
||||
}
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.scheme_preferences_appearance, rootKey)
|
||||
val attendanceTargetPref =
|
||||
findPreference<SeekBarPreference>(requireContext().getString(R.string.pref_key_attendance_target))!!
|
||||
attendanceTargetPref.setOnPreferenceChangeListener { _, newValueObj ->
|
||||
val newValue = (((newValueObj as Int).toDouble() + 2.5) / 5).toInt() * 5
|
||||
attendanceTargetPref.value =
|
||||
newValue.coerceIn(attendanceTargetPref.min, attendanceTargetPref.max)
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
|
||||
|
@ -7,27 +7,28 @@ import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.github.wulkanowy.R
|
||||
import io.github.wulkanowy.data.db.entities.Timetable
|
||||
import io.github.wulkanowy.databinding.ItemTimetableBinding
|
||||
import io.github.wulkanowy.databinding.ItemTimetableEmptyBinding
|
||||
import io.github.wulkanowy.databinding.ItemTimetableMainAdditionalBinding
|
||||
import io.github.wulkanowy.databinding.ItemTimetableSmallBinding
|
||||
import io.github.wulkanowy.utils.SyncListAdapter
|
||||
import io.github.wulkanowy.utils.getPlural
|
||||
import io.github.wulkanowy.utils.getThemeAttrColor
|
||||
import io.github.wulkanowy.utils.toFormattedString
|
||||
import javax.inject.Inject
|
||||
|
||||
class TimetableAdapter @Inject constructor() :
|
||||
ListAdapter<TimetableItem, RecyclerView.ViewHolder>(differ) {
|
||||
SyncListAdapter<TimetableItem, RecyclerView.ViewHolder>(Differ) {
|
||||
|
||||
override fun getItemViewType(position: Int): Int = getItem(position).type.ordinal
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
|
||||
return when (TimetableItemType.values()[viewType]) {
|
||||
return when (TimetableItemType.entries[viewType]) {
|
||||
TimetableItemType.SMALL -> SmallViewHolder(
|
||||
ItemTimetableSmallBinding.inflate(inflater, parent, false)
|
||||
)
|
||||
@ -39,6 +40,10 @@ class TimetableAdapter @Inject constructor() :
|
||||
TimetableItemType.EMPTY -> EmptyViewHolder(
|
||||
ItemTimetableEmptyBinding.inflate(inflater, parent, false)
|
||||
)
|
||||
|
||||
TimetableItemType.ADDITIONAL -> AdditionalViewHolder(
|
||||
ItemTimetableMainAdditionalBinding.inflate(inflater, parent, false)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -61,16 +66,30 @@ class TimetableAdapter @Inject constructor() :
|
||||
binding = holder.binding,
|
||||
item = getItem(position) as TimetableItem.Small,
|
||||
)
|
||||
|
||||
is NormalViewHolder -> bindNormalView(
|
||||
binding = holder.binding,
|
||||
item = getItem(position) as TimetableItem.Normal,
|
||||
)
|
||||
|
||||
is EmptyViewHolder -> bindEmptyView(
|
||||
binding = holder.binding,
|
||||
item = getItem(position) as TimetableItem.Empty,
|
||||
)
|
||||
|
||||
is AdditionalViewHolder -> bindAdditionalView(
|
||||
binding = holder.binding,
|
||||
item = getItem(position) as TimetableItem.Additional,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindAdditionalView(
|
||||
binding: ItemTimetableMainAdditionalBinding,
|
||||
item: TimetableItem.Additional
|
||||
) {
|
||||
with(binding) {
|
||||
timetableItemSubject.text = item.additional.subject
|
||||
timetableItemTimeStart.text = item.additional.start.toFormattedString("HH:mm")
|
||||
timetableItemTimeFinish.text = item.additional.end.toFormattedString("HH:mm")
|
||||
}
|
||||
}
|
||||
|
||||
@ -79,6 +98,7 @@ class TimetableAdapter @Inject constructor() :
|
||||
|
||||
with(binding) {
|
||||
timetableSmallItemNumber.text = lesson.number.toString()
|
||||
timetableSmallItemNumber.isVisible = item.isLessonNumberVisible
|
||||
timetableSmallItemSubject.text = lesson.subject
|
||||
timetableSmallItemTimeStart.text = lesson.start.toFormattedString("HH:mm")
|
||||
timetableSmallItemRoom.text = lesson.room
|
||||
@ -97,6 +117,7 @@ class TimetableAdapter @Inject constructor() :
|
||||
|
||||
with(binding) {
|
||||
timetableItemNumber.text = lesson.number.toString()
|
||||
timetableItemNumber.isVisible = item.isLessonNumberVisible
|
||||
timetableItemSubject.text = lesson.subject
|
||||
timetableItemGroup.text = lesson.group
|
||||
timetableItemRoom.text = lesson.room
|
||||
@ -305,31 +326,32 @@ class TimetableAdapter @Inject constructor() :
|
||||
private class EmptyViewHolder(val binding: ItemTimetableEmptyBinding) :
|
||||
RecyclerView.ViewHolder(binding.root)
|
||||
|
||||
companion object {
|
||||
private val differ = object : DiffUtil.ItemCallback<TimetableItem>() {
|
||||
override fun areItemsTheSame(oldItem: TimetableItem, newItem: TimetableItem): Boolean =
|
||||
when {
|
||||
oldItem is TimetableItem.Small && newItem is TimetableItem.Small -> {
|
||||
oldItem.lesson.start == newItem.lesson.start
|
||||
}
|
||||
private class AdditionalViewHolder(val binding: ItemTimetableMainAdditionalBinding) :
|
||||
RecyclerView.ViewHolder(binding.root)
|
||||
|
||||
oldItem is TimetableItem.Normal && newItem is TimetableItem.Normal -> {
|
||||
oldItem.lesson.start == newItem.lesson.start
|
||||
}
|
||||
|
||||
else -> oldItem == newItem
|
||||
private object Differ : DiffUtil.ItemCallback<TimetableItem>() {
|
||||
override fun areItemsTheSame(oldItem: TimetableItem, newItem: TimetableItem): Boolean =
|
||||
when {
|
||||
oldItem is TimetableItem.Small && newItem is TimetableItem.Small -> {
|
||||
oldItem.lesson.start == newItem.lesson.start
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: TimetableItem, newItem: TimetableItem) =
|
||||
oldItem == newItem
|
||||
oldItem is TimetableItem.Normal && newItem is TimetableItem.Normal -> {
|
||||
oldItem.lesson.start == newItem.lesson.start
|
||||
}
|
||||
|
||||
override fun getChangePayload(oldItem: TimetableItem, newItem: TimetableItem): Any? {
|
||||
return if (oldItem is TimetableItem.Normal && newItem is TimetableItem.Normal) {
|
||||
if (oldItem.lesson == newItem.lesson && oldItem.showGroupsInPlan == newItem.showGroupsInPlan && oldItem.timeLeft != newItem.timeLeft) {
|
||||
"time_left"
|
||||
} else super.getChangePayload(oldItem, newItem)
|
||||
} else super.getChangePayload(oldItem, newItem)
|
||||
else -> oldItem == newItem
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: TimetableItem, newItem: TimetableItem) =
|
||||
oldItem == newItem
|
||||
|
||||
override fun getChangePayload(oldItem: TimetableItem, newItem: TimetableItem): Any? {
|
||||
return if (oldItem is TimetableItem.Normal && newItem is TimetableItem.Normal) {
|
||||
if (oldItem.lesson == newItem.lesson && oldItem.showGroupsInPlan == newItem.showGroupsInPlan && oldItem.timeLeft != newItem.timeLeft) {
|
||||
"time_left"
|
||||
} else super.getChangePayload(oldItem, newItem)
|
||||
} else super.getChangePayload(oldItem, newItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,7 +21,11 @@ import io.github.wulkanowy.ui.modules.main.MainView
|
||||
import io.github.wulkanowy.ui.modules.timetable.additional.AdditionalLessonsFragment
|
||||
import io.github.wulkanowy.ui.modules.timetable.completed.CompletedLessonsFragment
|
||||
import io.github.wulkanowy.ui.widgets.DividerItemDecoration
|
||||
import io.github.wulkanowy.utils.*
|
||||
import io.github.wulkanowy.utils.dpToPx
|
||||
import io.github.wulkanowy.utils.firstSchoolDayInSchoolYear
|
||||
import io.github.wulkanowy.utils.getThemeAttrColor
|
||||
import io.github.wulkanowy.utils.lastSchoolDayInSchoolYear
|
||||
import io.github.wulkanowy.utils.openMaterialDatePicker
|
||||
import java.time.LocalDate
|
||||
import javax.inject.Inject
|
||||
|
||||
@ -104,8 +108,11 @@ class TimetableFragment : BaseFragment<FragmentTimetableBinding>(R.layout.fragme
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateData(data: List<TimetableItem>) {
|
||||
timetableAdapter.submitList(data)
|
||||
override fun updateData(data: List<TimetableItem>, isDayChanged: Boolean) {
|
||||
when {
|
||||
isDayChanged -> timetableAdapter.recreate(data)
|
||||
else -> timetableAdapter.submitList(data)
|
||||
}
|
||||
}
|
||||
|
||||
override fun clearData() {
|
||||
|
@ -1,18 +1,21 @@
|
||||
package io.github.wulkanowy.ui.modules.timetable
|
||||
|
||||
import io.github.wulkanowy.data.db.entities.Timetable
|
||||
import io.github.wulkanowy.data.db.entities.TimetableAdditional
|
||||
import java.time.Duration
|
||||
|
||||
sealed class TimetableItem(val type: TimetableItemType) {
|
||||
|
||||
data class Small(
|
||||
val lesson: Timetable,
|
||||
val isLessonNumberVisible: Boolean,
|
||||
val onClick: (Timetable) -> Unit,
|
||||
) : TimetableItem(TimetableItemType.SMALL)
|
||||
|
||||
data class Normal(
|
||||
val lesson: Timetable,
|
||||
val showGroupsInPlan: Boolean,
|
||||
val isLessonNumberVisible: Boolean,
|
||||
val timeLeft: TimeLeft?,
|
||||
val onClick: (Timetable) -> Unit,
|
||||
) : TimetableItem(TimetableItemType.NORMAL)
|
||||
@ -21,6 +24,10 @@ sealed class TimetableItem(val type: TimetableItemType) {
|
||||
val numFrom: Int,
|
||||
val numTo: Int
|
||||
) : TimetableItem(TimetableItemType.EMPTY)
|
||||
|
||||
data class Additional(
|
||||
val additional: TimetableAdditional,
|
||||
) : TimetableItem(TimetableItemType.ADDITIONAL)
|
||||
}
|
||||
|
||||
data class TimeLeft(
|
||||
@ -32,5 +39,6 @@ data class TimeLeft(
|
||||
enum class TimetableItemType {
|
||||
SMALL,
|
||||
NORMAL,
|
||||
EMPTY
|
||||
EMPTY,
|
||||
ADDITIONAL,
|
||||
}
|
||||
|
@ -4,6 +4,9 @@ import android.os.Handler
|
||||
import android.os.Looper
|
||||
import io.github.wulkanowy.data.db.entities.Semester
|
||||
import io.github.wulkanowy.data.db.entities.Timetable
|
||||
import io.github.wulkanowy.data.db.entities.TimetableAdditional
|
||||
import io.github.wulkanowy.data.enums.ShowAdditionalLessonsMode.BELOW
|
||||
import io.github.wulkanowy.data.enums.ShowAdditionalLessonsMode.NONE
|
||||
import io.github.wulkanowy.data.enums.TimetableGapsMode.BETWEEN_AND_BEFORE_LESSONS
|
||||
import io.github.wulkanowy.data.enums.TimetableGapsMode.NO_GAPS
|
||||
import io.github.wulkanowy.data.enums.TimetableMode
|
||||
@ -14,6 +17,7 @@ import io.github.wulkanowy.data.onResourceError
|
||||
import io.github.wulkanowy.data.onResourceIntermediate
|
||||
import io.github.wulkanowy.data.onResourceNotLoading
|
||||
import io.github.wulkanowy.data.onResourceSuccess
|
||||
import io.github.wulkanowy.data.pojos.TimetableFull
|
||||
import io.github.wulkanowy.data.repositories.PreferencesRepository
|
||||
import io.github.wulkanowy.data.repositories.SemesterRepository
|
||||
import io.github.wulkanowy.data.repositories.StudentRepository
|
||||
@ -57,6 +61,7 @@ class TimetablePresenter @Inject constructor(
|
||||
|
||||
private var initialDate: LocalDate? = null
|
||||
private var isWeekendHasLessons: Boolean = false
|
||||
private var isEduOne: Boolean = false
|
||||
|
||||
var currentDate: LocalDate? = null
|
||||
private set
|
||||
@ -80,7 +85,7 @@ class TimetablePresenter @Inject constructor(
|
||||
} else currentDate?.previousSchoolDay
|
||||
|
||||
reloadView(date ?: return)
|
||||
loadData()
|
||||
loadData(isDayChanged = true)
|
||||
}
|
||||
|
||||
fun onNextDay() {
|
||||
@ -89,7 +94,7 @@ class TimetablePresenter @Inject constructor(
|
||||
} else currentDate?.nextSchoolDay
|
||||
|
||||
reloadView(date ?: return)
|
||||
loadData()
|
||||
loadData(isDayChanged = true)
|
||||
}
|
||||
|
||||
fun onPickDate() {
|
||||
@ -103,7 +108,7 @@ class TimetablePresenter @Inject constructor(
|
||||
|
||||
fun onSwipeRefresh() {
|
||||
Timber.i("Force refreshing the timetable")
|
||||
loadData(true)
|
||||
loadData(forceRefresh = true)
|
||||
}
|
||||
|
||||
fun onRetry() {
|
||||
@ -111,7 +116,7 @@ class TimetablePresenter @Inject constructor(
|
||||
showErrorView(false)
|
||||
showProgress(true)
|
||||
}
|
||||
loadData(true)
|
||||
loadData(forceRefresh = true)
|
||||
}
|
||||
|
||||
fun onDetailsClick() {
|
||||
@ -144,11 +149,12 @@ class TimetablePresenter @Inject constructor(
|
||||
return true
|
||||
}
|
||||
|
||||
private fun loadData(forceRefresh: Boolean = false) {
|
||||
private fun loadData(forceRefresh: Boolean = false, isDayChanged: Boolean = false) {
|
||||
flatResourceFlow {
|
||||
val student = studentRepository.getCurrentStudent()
|
||||
val semester = semesterRepository.getCurrentSemester(student)
|
||||
|
||||
isEduOne = student.isEduOne == true
|
||||
checkInitialAndCurrentDate(semester)
|
||||
timetableRepository.getTimetable(
|
||||
student = student,
|
||||
@ -167,9 +173,9 @@ class TimetablePresenter @Inject constructor(
|
||||
enableSwipe(true)
|
||||
showProgress(false)
|
||||
showErrorView(false)
|
||||
showContent(it.lessons.isNotEmpty())
|
||||
showEmpty(it.lessons.isEmpty())
|
||||
updateData(it.lessons)
|
||||
updateData(it, isDayChanged)
|
||||
showContent(it.lessons.isNotEmpty() || it.additional.isNotEmpty())
|
||||
showEmpty(it.lessons.isEmpty() && it.additional.isEmpty())
|
||||
setDayHeaderMessage(it.headers.find { header -> header.date == currentDate }?.content)
|
||||
reloadNavigation()
|
||||
}
|
||||
@ -214,66 +220,97 @@ class TimetablePresenter @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateData(lessons: List<Timetable>) {
|
||||
private fun updateData(lessons: TimetableFull, isDayChanged: Boolean) {
|
||||
tickTimer?.cancel()
|
||||
|
||||
if (currentDate != now()) {
|
||||
view?.updateData(createItems(lessons))
|
||||
} else {
|
||||
tickTimer = timer(period = 2_000) {
|
||||
view?.updateData(createItems(lessons), isDayChanged)
|
||||
if (currentDate == now()) {
|
||||
tickTimer = timer(period = 2_000, initialDelay = 2_000) {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
view?.updateData(createItems(lessons))
|
||||
view?.updateData(createItems(lessons), isDayChanged)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createItems(items: List<Timetable>): List<TimetableItem> {
|
||||
val filteredItems = items
|
||||
.filter {
|
||||
if (prefRepository.showWholeClassPlan == TimetableMode.ONLY_CURRENT_GROUP) {
|
||||
it.isStudentPlan
|
||||
} else true
|
||||
}.sortedWith(
|
||||
compareBy({ item -> item.number }, { item -> !item.isStudentPlan })
|
||||
)
|
||||
private sealed class Item(
|
||||
val isStudentPlan: Boolean,
|
||||
val start: Instant,
|
||||
val number: Int?,
|
||||
) {
|
||||
class Lesson(val lesson: Timetable) :
|
||||
Item(lesson.isStudentPlan, lesson.start, lesson.number)
|
||||
|
||||
class Additional(val additional: TimetableAdditional) : Item(true, additional.start, null)
|
||||
}
|
||||
|
||||
private fun createItems(fullTimetable: TimetableFull): List<TimetableItem> {
|
||||
val showAdditionalLessonsInPlan = prefRepository.showAdditionalLessonsInPlan
|
||||
val allItems =
|
||||
fullTimetable.lessons.map(Item::Lesson) + fullTimetable.additional.map(Item::Additional)
|
||||
.takeIf { showAdditionalLessonsInPlan != NONE }.orEmpty()
|
||||
|
||||
val filteredItems = allItems.filter {
|
||||
if (prefRepository.showWholeClassPlan == TimetableMode.ONLY_CURRENT_GROUP) {
|
||||
it.isStudentPlan
|
||||
} else true
|
||||
}.sortedWith(
|
||||
(compareBy<Item> { it is Item.Additional }
|
||||
.takeIf { showAdditionalLessonsInPlan == BELOW } ?: EmptyComparator())
|
||||
.thenBy { it.start }
|
||||
.thenBy { !it.isStudentPlan }
|
||||
)
|
||||
|
||||
var prevNum = when (prefRepository.showTimetableGaps) {
|
||||
BETWEEN_AND_BEFORE_LESSONS -> 0
|
||||
else -> null
|
||||
}
|
||||
var prevIsAdditional = false
|
||||
return buildList {
|
||||
filteredItems.forEachIndexed { i, it ->
|
||||
if (prefRepository.showTimetableGaps != NO_GAPS && prevNum != null && it.number > prevNum!! + 1) {
|
||||
val emptyLesson = TimetableItem.Empty(
|
||||
numFrom = prevNum!! + 1,
|
||||
numTo = it.number - 1
|
||||
)
|
||||
add(emptyLesson)
|
||||
if (prefRepository.showTimetableGaps != NO_GAPS) {
|
||||
if (prevNum != null && it.number != null && it.number > prevNum!! + 1) {
|
||||
if (!prevIsAdditional) {
|
||||
// Additional lessons do count as a lesson so don't add empty lessons
|
||||
// when there is an additional lesson present
|
||||
val emptyLesson = TimetableItem.Empty(
|
||||
numFrom = prevNum!! + 1, numTo = it.number - 1
|
||||
)
|
||||
add(emptyLesson)
|
||||
}
|
||||
}
|
||||
prevNum = it.number
|
||||
prevIsAdditional = it is Item.Additional
|
||||
}
|
||||
|
||||
if (it.isStudentPlan) {
|
||||
val normalLesson = TimetableItem.Normal(
|
||||
lesson = it,
|
||||
showGroupsInPlan = prefRepository.showGroupsInPlan,
|
||||
timeLeft = filteredItems.getTimeLeftForLesson(it, i),
|
||||
onClick = ::onTimetableItemSelected
|
||||
)
|
||||
add(normalLesson)
|
||||
} else {
|
||||
val smallLesson = TimetableItem.Small(
|
||||
lesson = it,
|
||||
onClick = ::onTimetableItemSelected
|
||||
)
|
||||
add(smallLesson)
|
||||
if (it is Item.Lesson) {
|
||||
if (it.isStudentPlan) {
|
||||
val normalLesson = TimetableItem.Normal(
|
||||
lesson = it.lesson,
|
||||
showGroupsInPlan = prefRepository.showGroupsInPlan,
|
||||
timeLeft = filteredItems.getTimeLeftForLesson(it.lesson, i),
|
||||
onClick = ::onTimetableItemSelected,
|
||||
isLessonNumberVisible = !isEduOne
|
||||
)
|
||||
add(normalLesson)
|
||||
} else {
|
||||
val smallLesson = TimetableItem.Small(
|
||||
lesson = it.lesson,
|
||||
onClick = ::onTimetableItemSelected,
|
||||
isLessonNumberVisible = !isEduOne
|
||||
)
|
||||
add(smallLesson)
|
||||
}
|
||||
} else if (it is Item.Additional) {
|
||||
// If the user disabled showing additional lessons, they would've been filtered
|
||||
// out already, so there's no need to check it again.
|
||||
add(TimetableItem.Additional(it.additional))
|
||||
}
|
||||
|
||||
prevNum = it.number
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<Timetable>.getTimeLeftForLesson(lesson: Timetable, index: Int): TimeLeft {
|
||||
private fun List<Item>.getTimeLeftForLesson(lesson: Timetable, index: Int): TimeLeft {
|
||||
val isShowTimeUntil = lesson.isShowTimeUntil(getPreviousLesson(index))
|
||||
return TimeLeft(
|
||||
until = lesson.until.plusMinutes(1).takeIf { isShowTimeUntil },
|
||||
@ -282,11 +319,20 @@ class TimetablePresenter @Inject constructor(
|
||||
)
|
||||
}
|
||||
|
||||
private fun List<Timetable>.getPreviousLesson(position: Int): Instant? {
|
||||
return filter { it.isStudentPlan }
|
||||
.getOrNull(position - 1 - filterIndexed { i, item -> i < position && !item.isStudentPlan }.size)
|
||||
private fun List<Item>.getPreviousLesson(position: Int): Instant? {
|
||||
val lessonAdditionalOffset = filterIndexed { i, item ->
|
||||
i < position && item is Item.Additional
|
||||
}.size
|
||||
val lessonStudentPlanOffset = filterIndexed { i, item ->
|
||||
i < position && !item.isStudentPlan
|
||||
}.size
|
||||
val lessonIndex = position - 1 - lessonAdditionalOffset - lessonStudentPlanOffset
|
||||
|
||||
return filterIsInstance<Item.Lesson>()
|
||||
.filter { it.isStudentPlan }
|
||||
.getOrNull(lessonIndex)
|
||||
?.let {
|
||||
if (!it.canceled && it.isStudentPlan) it.end
|
||||
if (!it.lesson.canceled && it.isStudentPlan) it.lesson.end
|
||||
else null
|
||||
}
|
||||
}
|
||||
@ -339,3 +385,7 @@ class TimetablePresenter @Inject constructor(
|
||||
super.onDetachView()
|
||||
}
|
||||
}
|
||||
|
||||
private class EmptyComparator<T> : Comparator<T> {
|
||||
override fun compare(o1: T, o2: T) = 0
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ interface TimetableView : BaseView {
|
||||
|
||||
fun initView()
|
||||
|
||||
fun updateData(data: List<TimetableItem>)
|
||||
fun updateData(data: List<TimetableItem>, isDayChanged: Boolean)
|
||||
|
||||
fun updateNavigationDay(date: String)
|
||||
|
||||
|
@ -13,7 +13,11 @@ import io.github.wulkanowy.ui.modules.main.MainActivity
|
||||
import io.github.wulkanowy.ui.modules.main.MainView
|
||||
import io.github.wulkanowy.ui.modules.timetable.additional.add.AdditionalLessonAddDialog
|
||||
import io.github.wulkanowy.ui.widgets.DividerItemDecoration
|
||||
import io.github.wulkanowy.utils.*
|
||||
import io.github.wulkanowy.utils.dpToPx
|
||||
import io.github.wulkanowy.utils.firstSchoolDayInSchoolYear
|
||||
import io.github.wulkanowy.utils.getThemeAttrColor
|
||||
import io.github.wulkanowy.utils.lastSchoolDayInSchoolYear
|
||||
import io.github.wulkanowy.utils.openMaterialDatePicker
|
||||
import java.time.LocalDate
|
||||
import javax.inject.Inject
|
||||
|
||||
@ -132,8 +136,12 @@ class AdditionalLessonsFragment :
|
||||
binding.additionalLessonsNextButton.visibility = if (show) View.VISIBLE else View.INVISIBLE
|
||||
}
|
||||
|
||||
override fun showAddAdditionalLessonDialog() {
|
||||
(activity as? MainActivity)?.showDialogFragment(AdditionalLessonAddDialog.newInstance())
|
||||
override fun showAddAdditionalLessonDialog(currentDate: LocalDate) {
|
||||
(activity as? MainActivity)?.showDialogFragment(
|
||||
AdditionalLessonAddDialog.newInstance(
|
||||
currentDate
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun showDatePickerDialog(selectedDate: LocalDate) {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user