diff --git a/.circleci/config.yml b/.circleci/config.yml
index ce2922ba3..2cb2e1473 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -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
diff --git a/.github/workflows/deploy-store.yml b/.github/workflows/deploy-store.yml
index e8237a381..0195f3e56 100644
--- a/.github/workflows/deploy-store.yml
+++ b/.github/workflows/deploy-store.yml
@@ -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
diff --git a/.github/workflows/deploy-test.yml b/.github/workflows/deploy-test.yml
index c4f55e6af..42c1f8e7a 100644
--- a/.github/workflows/deploy-test.yml
+++ b/.github/workflows/deploy-test.yml
@@ -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:
diff --git a/.gitignore b/.gitignore
index 69d3ad5d2..810f5e7aa 100644
--- a/.gitignore
+++ b/.gitignore
@@ -71,6 +71,7 @@ captures/
.idea/deploymentTargetDropDown.xml
.idea/deploymentTargetSelector.xml
.idea/kotlinc.xml
+.idea/studiobot.xml
# Keystore files
*.jks
@@ -117,12 +118,14 @@ Thumbs.db
*.ear
### AndroidStudio Patch ###
-
!/gradle/wrapper/gradle-wrapper.jar
.idea/jarRepositories.xml
+### Services config files
+agconnect-services.json
+agconnect-credentials.json
+google-services.json
+!app/google-services.json
-app/src/release/agconnect-services.json
-app/src/release/agconnect-credentials.json
-.idea/deploymentTargetDropDown.xml
-.idea/kotlinc.xml
+
+.idea/appInsightsSettings.xml
diff --git a/.travis.yml b/.travis.yml
index 04db3a616..e0b0be978 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -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:
diff --git a/app/build.gradle b/app/build.gradle
index 01f4c3b16..f56d86955 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -27,15 +27,12 @@ android {
testApplicationId "io.github.tests.wulkanowy"
minSdkVersion 21
targetSdkVersion 34
- versionCode 154
- versionName "2.5.5"
+ versionCode 175
+ versionName "2.6.15"
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"'
}
@@ -165,7 +161,7 @@ play {
track = 'production'
releaseStatus = ReleaseStatus.IN_PROGRESS
userFraction = 0.99d
- updatePriority = 1
+ updatePriority = 3
enabled.set(false)
}
@@ -190,28 +186,30 @@ ext {
android_hilt = "1.2.0"
room = "2.6.1"
chucker = "4.0.0"
- mockk = "1.13.10"
- coroutines = "1.8.0"
+ mockk = "1.13.11"
+ coroutines = "1.8.1"
}
dependencies {
- implementation 'io.github.wulkanowy:sdk:2.5.5'
+ implementation 'io.github.wulkanowy:sdk:2.6.13'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines"
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-guava:$coroutines"
- implementation 'androidx.core:core-ktx:1.12.0'
+ implementation 'androidx.core:core-ktx:1.13.1'
implementation 'androidx.core:core-splashscreen:1.0.1'
- implementation "androidx.activity:activity-ktx:1.8.2"
+ implementation "androidx.activity:activity-ktx:1.9.0"
implementation "androidx.appcompat:appcompat:1.6.1"
- implementation "androidx.fragment:fragment-ktx:1.6.2"
- implementation "androidx.annotation:annotation:1.7.1"
+ implementation "androidx.fragment:fragment-ktx:1.7.1"
+ implementation "androidx.annotation:annotation:1.8.0"
+ implementation "androidx.javascriptengine:javascriptengine:1.0.0-beta01"
implementation "androidx.preference:preference-ktx:1.2.1"
implementation "androidx.recyclerview:recyclerview:1.3.2"
- implementation "androidx.viewpager2:viewpager2:1.1.0-beta02"
+ implementation "androidx.viewpager2:viewpager2:1.1.0"
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
implementation "androidx.constraintlayout:constraintlayout:2.1.4"
implementation "androidx.coordinatorlayout:coordinatorlayout:1.2.0"
@@ -223,7 +221,7 @@ dependencies {
implementation "androidx.work:work-runtime:$work_manager"
playImplementation "androidx.work:work-gcm:$work_manager"
- implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.7.0"
+ implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.8.0"
implementation "androidx.room:room-runtime:$room"
implementation "androidx.room:room-ktx:$room"
@@ -237,7 +235,7 @@ dependencies {
implementation 'com.github.ncapdevi:FragNav:3.3.0'
implementation "com.github.YarikSOffice:lingver:1.3.0"
- implementation 'com.squareup.retrofit2:retrofit:2.9.0'
+ implementation 'com.squareup.retrofit2:retrofit:2.11.0'
implementation "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0"
implementation "com.squareup.okhttp3:logging-interceptor:4.12.0"
implementation "com.squareup.okhttp3:okhttp-urlconnection:4.12.0"
@@ -250,9 +248,9 @@ dependencies {
implementation "io.github.wulkanowy:AppKillerManager:3.0.1"
implementation 'me.xdrop:fuzzywuzzy:1.4.0'
implementation 'com.fredporciuncula:flow-preferences:1.9.1'
- implementation 'org.apache.commons:commons-text:1.11.0'
+ implementation 'org.apache.commons:commons-text:1.12.0'
- playImplementation platform('com.google.firebase:firebase-bom:32.7.3')
+ playImplementation platform('com.google.firebase:firebase-bom:33.0.0')
playImplementation 'com.google.firebase:firebase-analytics'
playImplementation 'com.google.firebase:firebase-messaging'
playImplementation 'com.google.firebase:firebase-crashlytics:'
@@ -278,7 +276,7 @@ dependencies {
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines"
testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
- testImplementation 'org.robolectric:robolectric:4.11.1'
+ testImplementation 'org.robolectric:robolectric:4.12.2'
testImplementation "androidx.test:runner:1.5.2"
testImplementation "androidx.test.ext:junit:1.1.5"
testImplementation "androidx.test:core:1.5.0"
diff --git a/app/src/debug/google-services.json b/app/google-services.json
similarity index 56%
rename from app/src/debug/google-services.json
rename to app/google-services.json
index e9303986b..2f71b8549 100644
--- a/app/src/debug/google-services.json
+++ b/app/google-services.json
@@ -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"
diff --git a/app/play-publish-lint.sh b/app/play-publish-lint.sh
index d3354b1ad..5f0391de3 100755
--- a/app/play-publish-lint.sh
+++ b/app/play-publish-lint.sh
@@ -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
diff --git a/app/schemas/io.github.wulkanowy.data.db.AppDatabase/64.json b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/64.json
new file mode 100644
index 000000000..178a5eab5
--- /dev/null
+++ b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/64.json
@@ -0,0 +1,2559 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 64,
+ "identityHash": "dd5446e82ad8d0a65c545a5dbbaeb81c",
+ "entities": [
+ {
+ "tableName": "Students",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`scrapper_base_url` TEXT NOT NULL, `scrapper_domain_suffix` TEXT NOT NULL DEFAULT '', `mobile_base_url` TEXT NOT NULL, `login_type` TEXT NOT NULL, `login_mode` TEXT NOT NULL, `certificate_key` TEXT NOT NULL, `private_key` TEXT NOT NULL, `is_parent` INTEGER NOT NULL, `email` TEXT NOT NULL, `password` TEXT NOT NULL, `symbol` TEXT NOT NULL, `student_id` INTEGER NOT NULL, `user_login_id` INTEGER NOT NULL, `user_name` TEXT NOT NULL, `student_name` TEXT NOT NULL, `school_id` TEXT NOT NULL, `school_short` TEXT NOT NULL, `school_name` TEXT NOT NULL, `class_name` TEXT NOT NULL, `class_id` INTEGER NOT NULL, `is_current` INTEGER NOT NULL, `registration_date` INTEGER NOT NULL, `is_authorized` INTEGER NOT NULL DEFAULT 0, `is_edu_one` INTEGER DEFAULT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `nick` TEXT NOT NULL, `avatar_color` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "scrapperBaseUrl",
+ "columnName": "scrapper_base_url",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "scrapperDomainSuffix",
+ "columnName": "scrapper_domain_suffix",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "mobileBaseUrl",
+ "columnName": "mobile_base_url",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "loginType",
+ "columnName": "login_type",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "loginMode",
+ "columnName": "login_mode",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "certificateKey",
+ "columnName": "certificate_key",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "privateKey",
+ "columnName": "private_key",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isParent",
+ "columnName": "is_parent",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "email",
+ "columnName": "email",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "password",
+ "columnName": "password",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "symbol",
+ "columnName": "symbol",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "studentId",
+ "columnName": "student_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "userLoginId",
+ "columnName": "user_login_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "userName",
+ "columnName": "user_name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "studentName",
+ "columnName": "student_name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "schoolSymbol",
+ "columnName": "school_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "schoolShortName",
+ "columnName": "school_short",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "schoolName",
+ "columnName": "school_name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "className",
+ "columnName": "class_name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "classId",
+ "columnName": "class_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isCurrent",
+ "columnName": "is_current",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "registrationDate",
+ "columnName": "registration_date",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isAuthorized",
+ "columnName": "is_authorized",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "isEduOne",
+ "columnName": "is_edu_one",
+ "affinity": "INTEGER",
+ "notNull": false,
+ "defaultValue": "NULL"
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "nick",
+ "columnName": "nick",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "avatarColor",
+ "columnName": "avatar_color",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_Students_email_symbol_student_id_school_id_class_id",
+ "unique": true,
+ "columnNames": [
+ "email",
+ "symbol",
+ "student_id",
+ "school_id",
+ "class_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Students_email_symbol_student_id_school_id_class_id` ON `${TABLE_NAME}` (`email`, `symbol`, `student_id`, `school_id`, `class_id`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "Semesters",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `kindergarten_diary_id` INTEGER NOT NULL DEFAULT 0, `diary_name` TEXT NOT NULL, `school_year` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `semester_name` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `unit_id` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_current` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "studentId",
+ "columnName": "student_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "diaryId",
+ "columnName": "diary_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "kindergartenDiaryId",
+ "columnName": "kindergarten_diary_id",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "diaryName",
+ "columnName": "diary_name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "schoolYear",
+ "columnName": "school_year",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "semesterId",
+ "columnName": "semester_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "semesterName",
+ "columnName": "semester_name",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "start",
+ "columnName": "start",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "end",
+ "columnName": "end",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "classId",
+ "columnName": "class_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "unitId",
+ "columnName": "unit_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "current",
+ "columnName": "is_current",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_Semesters_student_id_diary_id_kindergarten_diary_id_semester_id",
+ "unique": true,
+ "columnNames": [
+ "student_id",
+ "diary_id",
+ "kindergarten_diary_id",
+ "semester_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Semesters_student_id_diary_id_kindergarten_diary_id_semester_id` ON `${TABLE_NAME}` (`student_id`, `diary_id`, `kindergarten_diary_id`, `semester_id`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "Exams",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `entry_date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `group` TEXT NOT NULL, `type` TEXT NOT NULL, `description` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "studentId",
+ "columnName": "student_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "diaryId",
+ "columnName": "diary_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "date",
+ "columnName": "date",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "entryDate",
+ "columnName": "entry_date",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "subject",
+ "columnName": "subject",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "group",
+ "columnName": "group",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "type",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "description",
+ "columnName": "description",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "teacher",
+ "columnName": "teacher",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "teacherSymbol",
+ "columnName": "teacher_symbol",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isNotified",
+ "columnName": "is_notified",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "Timetable",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `number` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `subjectOld` TEXT NOT NULL, `group` TEXT NOT NULL, `room` TEXT NOT NULL, `roomOld` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacherOld` TEXT NOT NULL, `info` TEXT NOT NULL, `student_plan` INTEGER NOT NULL, `changes` INTEGER NOT NULL, `canceled` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "studentId",
+ "columnName": "student_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "diaryId",
+ "columnName": "diary_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "number",
+ "columnName": "number",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "start",
+ "columnName": "start",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "end",
+ "columnName": "end",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "date",
+ "columnName": "date",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "subject",
+ "columnName": "subject",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "subjectOld",
+ "columnName": "subjectOld",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "group",
+ "columnName": "group",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "room",
+ "columnName": "room",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "roomOld",
+ "columnName": "roomOld",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "teacher",
+ "columnName": "teacher",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "teacherOld",
+ "columnName": "teacherOld",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "info",
+ "columnName": "info",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isStudentPlan",
+ "columnName": "student_plan",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "changes",
+ "columnName": "changes",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "canceled",
+ "columnName": "canceled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isNotified",
+ "columnName": "is_notified",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "Attendance",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `time_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `number` INTEGER NOT NULL, `subject` TEXT NOT NULL, `name` TEXT NOT NULL, `presence` INTEGER NOT NULL, `absence` INTEGER NOT NULL, `exemption` INTEGER NOT NULL, `lateness` INTEGER NOT NULL, `excused` INTEGER NOT NULL, `deleted` INTEGER NOT NULL, `excusable` INTEGER NOT NULL, `excuse_status` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "studentId",
+ "columnName": "student_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "diaryId",
+ "columnName": "diary_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "timeId",
+ "columnName": "time_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "date",
+ "columnName": "date",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "number",
+ "columnName": "number",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "subject",
+ "columnName": "subject",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "presence",
+ "columnName": "presence",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "absence",
+ "columnName": "absence",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "exemption",
+ "columnName": "exemption",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lateness",
+ "columnName": "lateness",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "excused",
+ "columnName": "excused",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "deleted",
+ "columnName": "deleted",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "excusable",
+ "columnName": "excusable",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "excuseStatus",
+ "columnName": "excuse_status",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isNotified",
+ "columnName": "is_notified",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "AttendanceSummary",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `subject_id` INTEGER NOT NULL, `month` INTEGER NOT NULL, `presence` INTEGER NOT NULL, `absence` INTEGER NOT NULL, `absence_excused` INTEGER NOT NULL, `absence_for_school_reasons` INTEGER NOT NULL, `lateness` INTEGER NOT NULL, `lateness_excused` INTEGER NOT NULL, `exemption` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "studentId",
+ "columnName": "student_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "diaryId",
+ "columnName": "diary_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "subjectId",
+ "columnName": "subject_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "month",
+ "columnName": "month",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "presence",
+ "columnName": "presence",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "absence",
+ "columnName": "absence",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "absenceExcused",
+ "columnName": "absence_excused",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "absenceForSchoolReasons",
+ "columnName": "absence_for_school_reasons",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lateness",
+ "columnName": "lateness",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "latenessExcused",
+ "columnName": "lateness_excused",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "exemption",
+ "columnName": "exemption",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "Grades",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `entry` TEXT NOT NULL, `value` REAL NOT NULL, `modifier` REAL NOT NULL, `comment` TEXT NOT NULL, `color` TEXT NOT NULL, `grade_symbol` TEXT NOT NULL, `description` TEXT NOT NULL, `weight` TEXT NOT NULL, `weightValue` REAL NOT NULL, `date` INTEGER NOT NULL, `teacher` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_read` INTEGER NOT NULL, `is_notified` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "semesterId",
+ "columnName": "semester_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "studentId",
+ "columnName": "student_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "subject",
+ "columnName": "subject",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "entry",
+ "columnName": "entry",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "value",
+ "columnName": "value",
+ "affinity": "REAL",
+ "notNull": true
+ },
+ {
+ "fieldPath": "modifier",
+ "columnName": "modifier",
+ "affinity": "REAL",
+ "notNull": true
+ },
+ {
+ "fieldPath": "comment",
+ "columnName": "comment",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "color",
+ "columnName": "color",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "gradeSymbol",
+ "columnName": "grade_symbol",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "description",
+ "columnName": "description",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "weight",
+ "columnName": "weight",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "weightValue",
+ "columnName": "weightValue",
+ "affinity": "REAL",
+ "notNull": true
+ },
+ {
+ "fieldPath": "date",
+ "columnName": "date",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "teacher",
+ "columnName": "teacher",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isRead",
+ "columnName": "is_read",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isNotified",
+ "columnName": "is_notified",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "GradesSummary",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `position` INTEGER NOT NULL, `subject` TEXT NOT NULL, `predicted_grade` TEXT NOT NULL, `final_grade` TEXT NOT NULL, `proposed_points` TEXT NOT NULL, `final_points` TEXT NOT NULL, `points_sum` TEXT NOT NULL, `points_sum_all_year` TEXT, `average` REAL NOT NULL, `average_all_year` REAL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_predicted_grade_notified` INTEGER NOT NULL, `is_final_grade_notified` INTEGER NOT NULL, `predicted_grade_last_change` INTEGER NOT NULL, `final_grade_last_change` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "semesterId",
+ "columnName": "semester_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "studentId",
+ "columnName": "student_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "position",
+ "columnName": "position",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "subject",
+ "columnName": "subject",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "predictedGrade",
+ "columnName": "predicted_grade",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "finalGrade",
+ "columnName": "final_grade",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "proposedPoints",
+ "columnName": "proposed_points",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "finalPoints",
+ "columnName": "final_points",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "pointsSum",
+ "columnName": "points_sum",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "pointsSumAllYear",
+ "columnName": "points_sum_all_year",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "average",
+ "columnName": "average",
+ "affinity": "REAL",
+ "notNull": true
+ },
+ {
+ "fieldPath": "averageAllYear",
+ "columnName": "average_all_year",
+ "affinity": "REAL",
+ "notNull": false
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isPredictedGradeNotified",
+ "columnName": "is_predicted_grade_notified",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isFinalGradeNotified",
+ "columnName": "is_final_grade_notified",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "predictedGradeLastChange",
+ "columnName": "predicted_grade_last_change",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "finalGradeLastChange",
+ "columnName": "final_grade_last_change",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "GradePartialStatistics",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `class_average` TEXT NOT NULL, `student_average` TEXT NOT NULL, `class_amounts` TEXT NOT NULL, `student_amounts` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "studentId",
+ "columnName": "student_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "semesterId",
+ "columnName": "semester_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "subject",
+ "columnName": "subject",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "classAverage",
+ "columnName": "class_average",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "studentAverage",
+ "columnName": "student_average",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "classAmounts",
+ "columnName": "class_amounts",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "studentAmounts",
+ "columnName": "student_amounts",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "GradesPointsStatistics",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `others` REAL NOT NULL, `student` REAL NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "studentId",
+ "columnName": "student_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "semesterId",
+ "columnName": "semester_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "subject",
+ "columnName": "subject",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "others",
+ "columnName": "others",
+ "affinity": "REAL",
+ "notNull": true
+ },
+ {
+ "fieldPath": "student",
+ "columnName": "student",
+ "affinity": "REAL",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "GradeSemesterStatistics",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `amounts` TEXT NOT NULL, `student_grade` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "studentId",
+ "columnName": "student_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "semesterId",
+ "columnName": "semester_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "subject",
+ "columnName": "subject",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "amounts",
+ "columnName": "amounts",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "studentGrade",
+ "columnName": "student_grade",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "Messages",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`email` TEXT NOT NULL, `message_global_key` TEXT NOT NULL, `mailbox_key` TEXT NOT NULL, `message_id` INTEGER NOT NULL, `correspondents` TEXT NOT NULL, `subject` TEXT NOT NULL, `date` INTEGER NOT NULL, `folder_id` INTEGER NOT NULL, `unread` INTEGER NOT NULL, `read_by` INTEGER, `unread_by` INTEGER, `has_attachments` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL, `content` TEXT NOT NULL, `sender` TEXT, `recipients` TEXT)",
+ "fields": [
+ {
+ "fieldPath": "email",
+ "columnName": "email",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "messageGlobalKey",
+ "columnName": "message_global_key",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "mailboxKey",
+ "columnName": "mailbox_key",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "messageId",
+ "columnName": "message_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "correspondents",
+ "columnName": "correspondents",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "subject",
+ "columnName": "subject",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "date",
+ "columnName": "date",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "folderId",
+ "columnName": "folder_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "unread",
+ "columnName": "unread",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "readBy",
+ "columnName": "read_by",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "unreadBy",
+ "columnName": "unread_by",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "hasAttachments",
+ "columnName": "has_attachments",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isNotified",
+ "columnName": "is_notified",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "content",
+ "columnName": "content",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "sender",
+ "columnName": "sender",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "recipients",
+ "columnName": "recipients",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "MessageAttachments",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`message_global_key` TEXT NOT NULL, `url` TEXT NOT NULL, `filename` TEXT NOT NULL, PRIMARY KEY(`message_global_key`, `url`, `filename`))",
+ "fields": [
+ {
+ "fieldPath": "messageGlobalKey",
+ "columnName": "message_global_key",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "url",
+ "columnName": "url",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "filename",
+ "columnName": "filename",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "message_global_key",
+ "url",
+ "filename"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "Notes",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `category` TEXT NOT NULL, `category_type` INTEGER NOT NULL, `is_points_show` INTEGER NOT NULL, `points` INTEGER NOT NULL, `content` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_read` INTEGER NOT NULL, `is_notified` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "studentId",
+ "columnName": "student_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "date",
+ "columnName": "date",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "teacher",
+ "columnName": "teacher",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "teacherSymbol",
+ "columnName": "teacher_symbol",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "category",
+ "columnName": "category",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "categoryType",
+ "columnName": "category_type",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isPointsShow",
+ "columnName": "is_points_show",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "points",
+ "columnName": "points",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "content",
+ "columnName": "content",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isRead",
+ "columnName": "is_read",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isNotified",
+ "columnName": "is_notified",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "Homework",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `entry_date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `content` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `attachments` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_done` INTEGER NOT NULL, `is_notified` INTEGER NOT NULL, `is_added_by_user` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "semesterId",
+ "columnName": "semester_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "studentId",
+ "columnName": "student_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "date",
+ "columnName": "date",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "entryDate",
+ "columnName": "entry_date",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "subject",
+ "columnName": "subject",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "content",
+ "columnName": "content",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "teacher",
+ "columnName": "teacher",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "teacherSymbol",
+ "columnName": "teacher_symbol",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "attachments",
+ "columnName": "attachments",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isDone",
+ "columnName": "is_done",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isNotified",
+ "columnName": "is_notified",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isAddedByUser",
+ "columnName": "is_added_by_user",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "Subjects",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `real_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "studentId",
+ "columnName": "student_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "diaryId",
+ "columnName": "diary_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "realId",
+ "columnName": "real_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "LuckyNumbers",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `lucky_number` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "studentId",
+ "columnName": "student_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "date",
+ "columnName": "date",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "luckyNumber",
+ "columnName": "lucky_number",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isNotified",
+ "columnName": "is_notified",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "CompletedLesson",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `number` INTEGER NOT NULL, `subject` TEXT NOT NULL, `topic` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `substitution` TEXT NOT NULL, `absence` TEXT NOT NULL, `resources` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "studentId",
+ "columnName": "student_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "diaryId",
+ "columnName": "diary_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "date",
+ "columnName": "date",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "number",
+ "columnName": "number",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "subject",
+ "columnName": "subject",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "topic",
+ "columnName": "topic",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "teacher",
+ "columnName": "teacher",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "teacherSymbol",
+ "columnName": "teacher_symbol",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "substitution",
+ "columnName": "substitution",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "absence",
+ "columnName": "absence",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "resources",
+ "columnName": "resources",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "Mailboxes",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`globalKey` TEXT NOT NULL, `email` TEXT NOT NULL, `symbol` TEXT NOT NULL, `schoolId` TEXT NOT NULL, `fullName` TEXT NOT NULL, `userName` TEXT NOT NULL, `studentName` TEXT NOT NULL, `schoolNameShort` TEXT NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`globalKey`))",
+ "fields": [
+ {
+ "fieldPath": "globalKey",
+ "columnName": "globalKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "email",
+ "columnName": "email",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "symbol",
+ "columnName": "symbol",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "schoolId",
+ "columnName": "schoolId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "fullName",
+ "columnName": "fullName",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "userName",
+ "columnName": "userName",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "studentName",
+ "columnName": "studentName",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "schoolNameShort",
+ "columnName": "schoolNameShort",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "type",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "globalKey"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "Recipients",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`mailboxGlobalKey` TEXT NOT NULL, `studentMailboxGlobalKey` TEXT NOT NULL, `fullName` TEXT NOT NULL, `userName` TEXT NOT NULL, `schoolShortName` TEXT NOT NULL, `type` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "mailboxGlobalKey",
+ "columnName": "mailboxGlobalKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "studentMailboxGlobalKey",
+ "columnName": "studentMailboxGlobalKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "fullName",
+ "columnName": "fullName",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "userName",
+ "columnName": "userName",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "schoolShortName",
+ "columnName": "schoolShortName",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "type",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "MobileDevices",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_login_id` INTEGER NOT NULL, `device_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `date` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "studentId",
+ "columnName": "user_login_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "deviceId",
+ "columnName": "device_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "date",
+ "columnName": "date",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "Teachers",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `name` TEXT NOT NULL, `short_name` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "studentId",
+ "columnName": "student_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "classId",
+ "columnName": "class_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "subject",
+ "columnName": "subject",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shortName",
+ "columnName": "short_name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "School",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `address` TEXT NOT NULL, `contact` TEXT NOT NULL, `headmaster` TEXT NOT NULL, `pedagogue` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "studentId",
+ "columnName": "student_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "classId",
+ "columnName": "class_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "address",
+ "columnName": "address",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "contact",
+ "columnName": "contact",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "headmaster",
+ "columnName": "headmaster",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "pedagogue",
+ "columnName": "pedagogue",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "Conferences",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `title` TEXT NOT NULL, `subject` TEXT NOT NULL, `agenda` TEXT NOT NULL, `present_on_conference` TEXT NOT NULL, `conference_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "studentId",
+ "columnName": "student_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "diaryId",
+ "columnName": "diary_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "title",
+ "columnName": "title",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "subject",
+ "columnName": "subject",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "agenda",
+ "columnName": "agenda",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "presentOnConference",
+ "columnName": "present_on_conference",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "conferenceId",
+ "columnName": "conference_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "date",
+ "columnName": "date",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isNotified",
+ "columnName": "is_notified",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "TimetableAdditional",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `repeat_id` BLOB DEFAULT NULL, `is_added_by_user` INTEGER NOT NULL DEFAULT 0)",
+ "fields": [
+ {
+ "fieldPath": "studentId",
+ "columnName": "student_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "diaryId",
+ "columnName": "diary_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "start",
+ "columnName": "start",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "end",
+ "columnName": "end",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "date",
+ "columnName": "date",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "subject",
+ "columnName": "subject",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "repeatId",
+ "columnName": "repeat_id",
+ "affinity": "BLOB",
+ "notNull": false,
+ "defaultValue": "NULL"
+ },
+ {
+ "fieldPath": "isAddedByUser",
+ "columnName": "is_added_by_user",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "StudentInfo",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `full_name` TEXT NOT NULL, `first_name` TEXT NOT NULL, `second_name` TEXT NOT NULL, `surname` TEXT NOT NULL, `birth_date` INTEGER NOT NULL, `birth_place` TEXT NOT NULL, `gender` TEXT NOT NULL, `has_polish_citizenship` INTEGER NOT NULL, `family_name` TEXT NOT NULL, `parents_names` TEXT NOT NULL, `address` TEXT NOT NULL, `registered_address` TEXT NOT NULL, `correspondence_address` TEXT NOT NULL, `phone_number` TEXT NOT NULL, `cell_phone_number` TEXT NOT NULL, `email` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `first_guardian_full_name` TEXT, `first_guardian_kinship` TEXT, `first_guardian_address` TEXT, `first_guardian_phones` TEXT, `first_guardian_email` TEXT, `second_guardian_full_name` TEXT, `second_guardian_kinship` TEXT, `second_guardian_address` TEXT, `second_guardian_phones` TEXT, `second_guardian_email` TEXT)",
+ "fields": [
+ {
+ "fieldPath": "studentId",
+ "columnName": "student_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "fullName",
+ "columnName": "full_name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "firstName",
+ "columnName": "first_name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "secondName",
+ "columnName": "second_name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "surname",
+ "columnName": "surname",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "birthDate",
+ "columnName": "birth_date",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "birthPlace",
+ "columnName": "birth_place",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "gender",
+ "columnName": "gender",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "hasPolishCitizenship",
+ "columnName": "has_polish_citizenship",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "familyName",
+ "columnName": "family_name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "parentsNames",
+ "columnName": "parents_names",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "address",
+ "columnName": "address",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "registeredAddress",
+ "columnName": "registered_address",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "correspondenceAddress",
+ "columnName": "correspondence_address",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "phoneNumber",
+ "columnName": "phone_number",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "cellPhoneNumber",
+ "columnName": "cell_phone_number",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "email",
+ "columnName": "email",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "firstGuardian.fullName",
+ "columnName": "first_guardian_full_name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "firstGuardian.kinship",
+ "columnName": "first_guardian_kinship",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "firstGuardian.address",
+ "columnName": "first_guardian_address",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "firstGuardian.phones",
+ "columnName": "first_guardian_phones",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "firstGuardian.email",
+ "columnName": "first_guardian_email",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "secondGuardian.fullName",
+ "columnName": "second_guardian_full_name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "secondGuardian.kinship",
+ "columnName": "second_guardian_kinship",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "secondGuardian.address",
+ "columnName": "second_guardian_address",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "secondGuardian.phones",
+ "columnName": "second_guardian_phones",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "secondGuardian.email",
+ "columnName": "second_guardian_email",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "TimetableHeaders",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `content` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "studentId",
+ "columnName": "student_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "diaryId",
+ "columnName": "diary_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "date",
+ "columnName": "date",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "content",
+ "columnName": "content",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "SchoolAnnouncements",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_login_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `content` TEXT NOT NULL, `author` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "studentId",
+ "columnName": "user_login_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "date",
+ "columnName": "date",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "subject",
+ "columnName": "subject",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "content",
+ "columnName": "content",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "author",
+ "columnName": "author",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isNotified",
+ "columnName": "is_notified",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "Notifications",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `type` TEXT NOT NULL, `destination` TEXT NOT NULL DEFAULT '{\"type\":\"io.github.wulkanowy.ui.modules.Destination.Dashboard\"}', `date` INTEGER NOT NULL, `data` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "studentId",
+ "columnName": "student_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "title",
+ "columnName": "title",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "content",
+ "columnName": "content",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "type",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "destination",
+ "columnName": "destination",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "'{\"type\":\"io.github.wulkanowy.ui.modules.Destination.Dashboard\"}'"
+ },
+ {
+ "fieldPath": "date",
+ "columnName": "date",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "data",
+ "columnName": "data",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "AdminMessages",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `version_name` INTEGER, `version_max` INTEGER, `target_register_host` TEXT, `target_flavor` TEXT, `destination_url` TEXT, `priority` TEXT NOT NULL, `types` TEXT NOT NULL DEFAULT '[]', `is_ok_visible` INTEGER NOT NULL DEFAULT 0, `is_x_visible` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "title",
+ "columnName": "title",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "content",
+ "columnName": "content",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "versionMin",
+ "columnName": "version_name",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "versionMax",
+ "columnName": "version_max",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "targetRegisterHost",
+ "columnName": "target_register_host",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "targetFlavor",
+ "columnName": "target_flavor",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "destinationUrl",
+ "columnName": "destination_url",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "priority",
+ "columnName": "priority",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "types",
+ "columnName": "types",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "'[]'"
+ },
+ {
+ "fieldPath": "isOkVisible",
+ "columnName": "is_ok_visible",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "isXVisible",
+ "columnName": "is_x_visible",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "MutedMessageSenders",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`author` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "author",
+ "columnName": "author",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "GradesDescriptive",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `description` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "semesterId",
+ "columnName": "semester_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "studentId",
+ "columnName": "student_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "subject",
+ "columnName": "subject",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "description",
+ "columnName": "description",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isNotified",
+ "columnName": "is_notified",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'dd5446e82ad8d0a65c545a5dbbaeb81c')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/src/debug/agconnect-services.json b/app/src/debug/agconnect-services.json
deleted file mode 100644
index 52426f54e..000000000
--- a/app/src/debug/agconnect-services.json
+++ /dev/null
@@ -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"
- }
- }
- ]
-}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index f43dfdd2c..a4257893f 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -3,6 +3,8 @@
xmlns:tools="http://schemas.android.com/tools"
android:installLocation="internalOnly">
+
+
@@ -42,16 +44,16 @@
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
+ android:resizeableActivity="true"
android:supportsRtl="false"
android:theme="@style/WulkanowyTheme"
- android:resizeableActivity="true"
tools:ignore="DataExtractionRules,UnusedAttribute">
+ tools:ignore="DiscouragedApi,LockedOrientationActivity">
@@ -155,33 +157,9 @@
android:resource="@xml/provider_paths" />
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/java/io/github/wulkanowy/data/DataModule.kt b/app/src/main/java/io/github/wulkanowy/data/DataModule.kt
index a492c08db..a21d56ccc 100644
--- a/app/src/main/java/io/github/wulkanowy/data/DataModule.kt
+++ b/app/src/main/java/io/github/wulkanowy/data/DataModule.kt
@@ -13,8 +13,8 @@ import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
-import io.github.wulkanowy.data.api.AdminMessageService
-import io.github.wulkanowy.data.api.SchoolsService
+import io.github.wulkanowy.data.api.services.SchoolsService
+import io.github.wulkanowy.data.api.services.WulkanowyService
import io.github.wulkanowy.data.db.AppDatabase
import io.github.wulkanowy.data.db.SharedPrefProvider
import io.github.wulkanowy.data.repositories.PreferencesRepository
@@ -71,7 +71,7 @@ internal class DataModule {
okHttpClient: OkHttpClient,
json: Json,
appInfo: AppInfo
- ): AdminMessageService = Retrofit.Builder()
+ ): WulkanowyService = Retrofit.Builder()
.baseUrl(appInfo.messagesBaseUrl)
.client(okHttpClient)
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
diff --git a/app/src/main/java/io/github/wulkanowy/data/Resource.kt b/app/src/main/java/io/github/wulkanowy/data/Resource.kt
index 108b0d58e..712a946f3 100644
--- a/app/src/main/java/io/github/wulkanowy/data/Resource.kt
+++ b/app/src/main/java/io/github/wulkanowy/data/Resource.kt
@@ -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 {
-
- open class Loading : Resource()
+sealed interface Resource {
+ /**
+ * 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 : Resource
+ /**
+ * 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(val data: T) : Loading()
- data class Success(val data: T) : Resource()
+ /**
+ * 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(val data: T) : Resource
- data class Error(val error: Throwable) : Resource()
+ /**
+ * 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(val error: Throwable) : Resource
}
val Resource.dataOrNull: T?
@@ -64,6 +93,22 @@ fun Resource.mapData(block: (T) -> U) = when (this) {
is Resource.Error -> Resource.Error(this.error)
}
+/**
+ * Injects another flow into this flow's resource data.
+ */
+inline fun Flow>.combineWithResourceData(
+ flow: Flow,
+ crossinline block: suspend (T1, T2) -> R
+): Flow> =
+ 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 Flow>.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 Flow>.logResourceStatus(name: String, showData: Boolean = fa
Timber.i("$name: $description")
}
-fun Flow>.mapResourceData(block: (T) -> U) = map {
- it.mapData(block)
+inline fun Flow>.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 Flow>.flatMapResourceData(
+ inheritIntermediate: Boolean = true, block: suspend (T) -> Flow>
+) = 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 Flow>.onResourceData(block: suspend (T) -> Unit) = onEach {
@@ -105,13 +171,13 @@ fun Flow>.onResourceSuccess(block: suspend (T) -> Unit) = onEach
}
}
-fun Flow>.onResourceError(block: (Throwable) -> Unit) = onEach {
+fun Flow>.onResourceError(block: suspend (Throwable) -> Unit) = onEach {
if (it is Resource.Error) {
block(it.error)
}
}
-fun Flow>.onResourceNotLoading(block: () -> Unit) = onEach {
+fun Flow>.onResourceNotLoading(block: suspend () -> Unit) = onEach {
if (it !is Resource.Loading) {
block()
}
@@ -121,70 +187,99 @@ suspend fun Flow>.toFirstResult() = filter { it !is Resource.Loa
suspend fun Flow>.waitForResult() = takeWhile { it is Resource.Loading }.collect()
-inline fun networkBoundResource(
- mutex: Mutex = Mutex(),
- showSavedOnLoading: Boolean = true,
- crossinline isResultEmpty: (ResultType) -> Boolean,
- crossinline query: () -> Flow,
- 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 combineResourceFlows(flows: Iterable>>): Flow>> =
+ combine(flows) { items ->
+ var isIntermediate = false
+ val data = mutableListOf()
+ 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 Flow>.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 networkBoundResource(
+ mutex: Mutex = Mutex(),
+ crossinline isResultEmpty: (OutputType) -> Boolean,
+ crossinline query: () -> Flow,
+ 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 networkBoundResource(
+inline fun networkBoundResource(
mutex: Mutex = Mutex(),
- showSavedOnLoading: Boolean = true,
- crossinline isResultEmpty: (T) -> Boolean,
- crossinline query: () -> Flow,
- 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,
+ 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) }
diff --git a/app/src/main/java/io/github/wulkanowy/data/WulkanowySdkFactory.kt b/app/src/main/java/io/github/wulkanowy/data/WulkanowySdkFactory.kt
index 83268a0e5..6b8555e43 100644
--- a/app/src/main/java/io/github/wulkanowy/data/WulkanowySdkFactory.kt
+++ b/app/src/main/java/io/github/wulkanowy/data/WulkanowySdkFactory.kt
@@ -1,13 +1,21 @@
package io.github.wulkanowy.data
+import android.content.Context
+import android.os.Build
+import androidx.javascriptengine.JavaScriptSandbox
import com.chuckerteam.chucker.api.ChuckerInterceptor
+import com.google.common.util.concurrent.ListenableFuture
+import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.data.db.dao.StudentDao
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentIsEduOne
+import io.github.wulkanowy.data.repositories.WulkanowyRepository
import io.github.wulkanowy.sdk.Sdk
+import io.github.wulkanowy.sdk.scrapper.EvaluateHandler
import io.github.wulkanowy.utils.RemoteConfigHelper
import io.github.wulkanowy.utils.WebkitCookieManagerProxy
+import kotlinx.coroutines.guava.await
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import timber.log.Timber
@@ -16,18 +24,24 @@ import javax.inject.Singleton
@Singleton
class WulkanowySdkFactory @Inject constructor(
+ @ApplicationContext private val context: Context,
private val chuckerInterceptor: ChuckerInterceptor,
private val remoteConfig: RemoteConfigHelper,
private val webkitCookieManagerProxy: WebkitCookieManagerProxy,
private val studentDb: StudentDao,
+ private val wulkanowyRepository: WulkanowyRepository,
) {
private val eduOneMutex = Mutex()
private val migrationFailedStudentIds = mutableSetOf()
+ private val sandbox: ListenableFuture? =
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && JavaScriptSandbox.isSupported())
+ JavaScriptSandbox.createConnectedInstanceAsync(context)
+ else null
private val sdk = Sdk().apply {
- androidVersion = android.os.Build.VERSION.RELEASE
- buildTag = android.os.Build.MODEL
+ androidVersion = Build.VERSION.RELEASE
+ buildTag = Build.MODEL
userAgentTemplate = remoteConfig.userAgentTemplate
setSimpleHttpLogger { Timber.d(it) }
setAdditionalCookieManager(webkitCookieManagerProxy)
@@ -36,14 +50,47 @@ class WulkanowySdkFactory @Inject constructor(
addInterceptor(chuckerInterceptor, network = true)
}
- fun create() = sdk
+ fun createBase() = sdk
+
+ suspend fun create(): Sdk {
+ val mapping = wulkanowyRepository.getMapping()
+
+ return createBase().apply {
+ if (mapping != null) {
+ endpointsMapping = mapping.endpoints
+ vTokenMapping = mapping.vTokens
+ vHeaders = mapping.vHeaders
+ responseMapping = mapping.responseMap
+ vParamsEvaluation = createIsolate()
+ }
+ }
+ }
+
+ private suspend fun createIsolate(): suspend () -> EvaluateHandler {
+ return {
+ val isolate = sandbox?.await()?.createIsolate()
+ object : EvaluateHandler {
+ override suspend fun evaluate(code: String): String? {
+ return isolate?.evaluateJavaScriptAsync(code)?.await()
+ }
+
+ override fun close() {
+ isolate?.close()
+ }
+ }
+ }
+ }
suspend fun create(student: Student, semester: Semester? = null): Sdk {
val overrideIsEduOne = checkEduOneAndMigrateIfNecessary(student)
return buildSdk(student, semester, overrideIsEduOne)
}
- private fun buildSdk(student: Student, semester: Semester?, isStudentEduOne: Boolean): Sdk {
+ private suspend fun buildSdk(
+ student: Student,
+ semester: Semester?,
+ isStudentEduOne: Boolean
+ ): Sdk {
return create().apply {
email = student.email
password = student.password
diff --git a/app/src/main/java/io/github/wulkanowy/data/api/models/Mapping.kt b/app/src/main/java/io/github/wulkanowy/data/api/models/Mapping.kt
new file mode 100644
index 000000000..e80f7cda4
--- /dev/null
+++ b/app/src/main/java/io/github/wulkanowy/data/api/models/Mapping.kt
@@ -0,0 +1,23 @@
+package io.github.wulkanowy.data.api.models
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class Mapping(
+
+ @SerialName("endpoints")
+ val endpoints: Map>>,
+
+ @SerialName("vTokens")
+ val vTokens: Map>>,
+
+ @SerialName("vTokenScheme")
+ val vTokenScheme: Map> = emptyMap(),
+
+ @SerialName("vHeaders")
+ val vHeaders: Map>> = emptyMap(),
+
+ @SerialName("responseMap")
+ val responseMap: Map>>> = emptyMap(),
+)
diff --git a/app/src/main/java/io/github/wulkanowy/data/api/SchoolsService.kt b/app/src/main/java/io/github/wulkanowy/data/api/services/SchoolsService.kt
similarity index 87%
rename from app/src/main/java/io/github/wulkanowy/data/api/SchoolsService.kt
rename to app/src/main/java/io/github/wulkanowy/data/api/services/SchoolsService.kt
index a7da9b63c..07fbeb896 100644
--- a/app/src/main/java/io/github/wulkanowy/data/api/SchoolsService.kt
+++ b/app/src/main/java/io/github/wulkanowy/data/api/services/SchoolsService.kt
@@ -1,4 +1,4 @@
-package io.github.wulkanowy.data.api
+package io.github.wulkanowy.data.api.services
import io.github.wulkanowy.data.pojos.IntegrityRequest
import io.github.wulkanowy.data.pojos.LoginEvent
diff --git a/app/src/main/java/io/github/wulkanowy/data/api/AdminMessageService.kt b/app/src/main/java/io/github/wulkanowy/data/api/services/WulkanowyService.kt
similarity index 51%
rename from app/src/main/java/io/github/wulkanowy/data/api/AdminMessageService.kt
rename to app/src/main/java/io/github/wulkanowy/data/api/services/WulkanowyService.kt
index 23f5af24a..e780bdd22 100644
--- a/app/src/main/java/io/github/wulkanowy/data/api/AdminMessageService.kt
+++ b/app/src/main/java/io/github/wulkanowy/data/api/services/WulkanowyService.kt
@@ -1,12 +1,16 @@
-package io.github.wulkanowy.data.api
+package io.github.wulkanowy.data.api.services
+import io.github.wulkanowy.data.api.models.Mapping
import io.github.wulkanowy.data.db.entities.AdminMessage
import retrofit2.http.GET
import javax.inject.Singleton
@Singleton
-interface AdminMessageService {
+interface WulkanowyService {
@GET("/v1.json")
suspend fun getAdminMessages(): List
-}
\ No newline at end of file
+
+ @GET("/mapping4.json")
+ suspend fun getMapping(): Mapping
+}
diff --git a/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt b/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt
index ec22c5a3d..f23c79de0 100644
--- a/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt
+++ b/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt
@@ -177,6 +177,7 @@ import javax.inject.Singleton
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
@@ -185,7 +186,7 @@ import javax.inject.Singleton
abstract class AppDatabase : RoomDatabase() {
companion object {
- const val VERSION_SCHEMA = 63
+ const val VERSION_SCHEMA = 64
fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf(
Migration2(),
diff --git a/app/src/main/java/io/github/wulkanowy/data/db/dao/MobileDeviceDao.kt b/app/src/main/java/io/github/wulkanowy/data/db/dao/MobileDeviceDao.kt
index 96382cc10..5ddb4dd08 100644
--- a/app/src/main/java/io/github/wulkanowy/data/db/dao/MobileDeviceDao.kt
+++ b/app/src/main/java/io/github/wulkanowy/data/db/dao/MobileDeviceDao.kt
@@ -8,6 +8,6 @@ import kotlinx.coroutines.flow.Flow
@Dao
interface MobileDeviceDao : BaseDao {
- @Query("SELECT * FROM MobileDevices WHERE user_login_id = :userLoginId ORDER BY date DESC")
- fun loadAll(userLoginId: Int): Flow>
+ @Query("SELECT * FROM MobileDevices WHERE user_login_id = :studentId ORDER BY date DESC")
+ fun loadAll(studentId: Int): Flow>
}
diff --git a/app/src/main/java/io/github/wulkanowy/data/db/dao/SchoolAnnouncementDao.kt b/app/src/main/java/io/github/wulkanowy/data/db/dao/SchoolAnnouncementDao.kt
index c32e4aba3..64d49bce7 100644
--- a/app/src/main/java/io/github/wulkanowy/data/db/dao/SchoolAnnouncementDao.kt
+++ b/app/src/main/java/io/github/wulkanowy/data/db/dao/SchoolAnnouncementDao.kt
@@ -10,6 +10,6 @@ import javax.inject.Singleton
@Singleton
interface SchoolAnnouncementDao : BaseDao {
- @Query("SELECT * FROM SchoolAnnouncements WHERE user_login_id = :userLoginId ORDER BY date DESC")
- fun loadAll(userLoginId: Int): Flow>
+ @Query("SELECT * FROM SchoolAnnouncements WHERE user_login_id = :studentId ORDER BY date DESC")
+ fun loadAll(studentId: Int): Flow>
}
diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/AdminMessage.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/AdminMessage.kt
index 0c8f1a5d1..a8604c5c1 100644
--- a/app/src/main/java/io/github/wulkanowy/data/db/entities/AdminMessage.kt
+++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/AdminMessage.kt
@@ -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 = emptyList(),
diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/GradeSummary.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/GradeSummary.kt
index a42832ced..f8a357a39 100644
--- a/app/src/main/java/io/github/wulkanowy/data/db/entities/GradeSummary.kt
+++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/GradeSummary.kt
@@ -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
diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/MobileDevice.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/MobileDevice.kt
index 89b04ccc8..44e900647 100644
--- a/app/src/main/java/io/github/wulkanowy/data/db/entities/MobileDevice.kt
+++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/MobileDevice.kt
@@ -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,
diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/SchoolAnnouncement.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/SchoolAnnouncement.kt
index ac096b02b..814a3c8dd 100644
--- a/app/src/main/java/io/github/wulkanowy/data/db/entities/SchoolAnnouncement.kt
+++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/SchoolAnnouncement.kt
@@ -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,
diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/Student.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/Student.kt
index dbaa573ce..0300506ac 100644
--- a/app/src/main/java/io/github/wulkanowy/data/db/entities/Student.kt
+++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/Student.kt
@@ -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,
diff --git a/app/src/main/java/io/github/wulkanowy/data/enums/AttendanceCalculatorSortingMode.kt b/app/src/main/java/io/github/wulkanowy/data/enums/AttendanceCalculatorSortingMode.kt
new file mode 100644
index 000000000..77dd5fc4b
--- /dev/null
+++ b/app/src/main/java/io/github/wulkanowy/data/enums/AttendanceCalculatorSortingMode.kt
@@ -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
+ }
+}
diff --git a/app/src/main/java/io/github/wulkanowy/data/enums/MessageType.kt b/app/src/main/java/io/github/wulkanowy/data/enums/MessageType.kt
index 531684e4e..ecd8d916f 100644
--- a/app/src/main/java/io/github/wulkanowy/data/enums/MessageType.kt
+++ b/app/src/main/java/io/github/wulkanowy/data/enums/MessageType.kt
@@ -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,
}
diff --git a/app/src/main/java/io/github/wulkanowy/data/enums/ShowAdditionalLessonsMode.kt b/app/src/main/java/io/github/wulkanowy/data/enums/ShowAdditionalLessonsMode.kt
new file mode 100644
index 000000000..3e7cdef5b
--- /dev/null
+++ b/app/src/main/java/io/github/wulkanowy/data/enums/ShowAdditionalLessonsMode.kt
@@ -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
+ }
+}
diff --git a/app/src/main/java/io/github/wulkanowy/data/mappers/DirectorInformationMapper.kt b/app/src/main/java/io/github/wulkanowy/data/mappers/DirectorInformationMapper.kt
index 85b37afc1..1a84a6a5e 100644
--- a/app/src/main/java/io/github/wulkanowy/data/mappers/DirectorInformationMapper.kt
+++ b/app/src/main/java/io/github/wulkanowy/data/mappers/DirectorInformationMapper.kt
@@ -8,7 +8,7 @@ import io.github.wulkanowy.sdk.pojo.LastAnnouncement as SdkLastAnnouncement
@JvmName("mapDirectorInformationToEntities")
fun List.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.mapToEntities(student: Student) = map {
@JvmName("mapLastAnnouncementsToEntities")
fun List.mapToEntities(student: Student) = map {
SchoolAnnouncement(
- userLoginId = student.userLoginId,
+ studentId = student.studentId,
date = it.date,
subject = it.subject,
content = it.content,
diff --git a/app/src/main/java/io/github/wulkanowy/data/mappers/GradeMapper.kt b/app/src/main/java/io/github/wulkanowy/data/mappers/GradeMapper.kt
index 66e922171..57322a7ae 100644
--- a/app/src/main/java/io/github/wulkanowy/data/mappers/GradeMapper.kt
+++ b/app/src/main/java/io/github/wulkanowy/data/mappers/GradeMapper.kt
@@ -37,9 +37,11 @@ fun List.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,
)
}
diff --git a/app/src/main/java/io/github/wulkanowy/data/mappers/MobileDeviceMapper.kt b/app/src/main/java/io/github/wulkanowy/data/mappers/MobileDeviceMapper.kt
index 1f4178fae..3818f01aa 100644
--- a/app/src/main/java/io/github/wulkanowy/data/mappers/MobileDeviceMapper.kt
+++ b/app/src/main/java/io/github/wulkanowy/data/mappers/MobileDeviceMapper.kt
@@ -8,7 +8,7 @@ import io.github.wulkanowy.sdk.pojo.Token as SdkToken
fun List.mapToEntities(student: Student) = map {
MobileDevice(
- userLoginId = student.userLoginId,
+ studentId = student.studentId,
date = it.createDate.toInstant(),
deviceId = it.id,
name = it.name
diff --git a/app/src/main/java/io/github/wulkanowy/data/pojos/AttendanceData.kt b/app/src/main/java/io/github/wulkanowy/data/pojos/AttendanceData.kt
new file mode 100644
index 000000000..5810363c6
--- /dev/null
+++ b/app/src/main/java/io/github/wulkanowy/data/pojos/AttendanceData.kt
@@ -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
+}
diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/AdminMessageRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/AdminMessageRepository.kt
deleted file mode 100644
index b831ee755..000000000
--- a/app/src/main/java/io/github/wulkanowy/data/repositories/AdminMessageRepository.kt
+++ /dev/null
@@ -1,33 +0,0 @@
-package io.github.wulkanowy.data.repositories
-
-import io.github.wulkanowy.data.Resource
-import io.github.wulkanowy.data.api.AdminMessageService
-import io.github.wulkanowy.data.db.dao.AdminMessageDao
-import io.github.wulkanowy.data.db.entities.AdminMessage
-import io.github.wulkanowy.data.networkBoundResource
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.sync.Mutex
-import javax.inject.Inject
-import javax.inject.Singleton
-
-@Singleton
-class AdminMessageRepository @Inject constructor(
- private val adminMessageService: AdminMessageService,
- private val adminMessageDao: AdminMessageDao,
-) {
-
- private val saveFetchResultMutex = Mutex()
-
- fun getAdminMessages(): Flow>> =
- networkBoundResource(
- mutex = saveFetchResultMutex,
- isResultEmpty = { false },
- query = { adminMessageDao.loadAll() },
- fetch = { adminMessageService.getAdminMessages() },
- shouldFetch = { true },
- saveFetchResult = { oldItems, newItems ->
- adminMessageDao.removeOldAndSaveNew(oldItems, newItems)
- },
- showSavedOnLoading = false,
- )
-}
diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/LuckyNumberRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/LuckyNumberRepository.kt
index 3636cb51e..dfafe6c8a 100644
--- a/app/src/main/java/io/github/wulkanowy/data/repositories/LuckyNumberRepository.kt
+++ b/app/src/main/java/io/github/wulkanowy/data/repositories/LuckyNumberRepository.kt
@@ -6,6 +6,8 @@ import io.github.wulkanowy.data.db.entities.LuckyNumber
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.mappers.mapToEntity
import io.github.wulkanowy.data.networkBoundResource
+import io.github.wulkanowy.ui.modules.luckynumberwidget.LuckyNumberWidgetProvider
+import io.github.wulkanowy.utils.AppWidgetUpdater
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.sync.Mutex
@@ -18,6 +20,7 @@ import javax.inject.Singleton
class LuckyNumberRepository @Inject constructor(
private val luckyNumberDb: LuckyNumberDao,
private val wulkanowySdkFactory: WulkanowySdkFactory,
+ private val appWidgetUpdater: AppWidgetUpdater,
) {
private val saveFetchResultMutex = Mutex()
@@ -26,6 +29,7 @@ class LuckyNumberRepository @Inject constructor(
student: Student,
forceRefresh: Boolean,
notify: Boolean = false,
+ isFromAppWidget: Boolean = false
) = networkBoundResource(
mutex = saveFetchResultMutex,
isResultEmpty = { it == null },
@@ -44,6 +48,9 @@ class LuckyNumberRepository @Inject constructor(
oldItems = listOfNotNull(oldLuckyNumber),
newItems = listOf(newLuckyNumber.apply { if (notify) isNotified = false }),
)
+ if (!isFromAppWidget) {
+ appWidgetUpdater.updateAllAppWidgetsByProvider(LuckyNumberWidgetProvider::class)
+ }
}
}
)
diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt
index ede2a0fde..f91dc63e3 100644
--- a/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt
+++ b/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt
@@ -122,7 +122,7 @@ class MessageRepository @Inject constructor(
fetch = {
wulkanowySdkFactory.create(student)
.getMessageDetails(
- messageKey = it!!.message.messageGlobalKey,
+ messageKey = message.messageGlobalKey,
markAsRead = message.unread && markAsRead,
)
},
diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/MobileDeviceRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/MobileDeviceRepository.kt
index 19466554a..1303d0e7a 100644
--- a/app/src/main/java/io/github/wulkanowy/data/repositories/MobileDeviceRepository.kt
+++ b/app/src/main/java/io/github/wulkanowy/data/repositories/MobileDeviceRepository.kt
@@ -38,7 +38,7 @@ 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 = {
wulkanowySdkFactory.create(student, semester)
.getRegisteredDevices()
diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt
index 64e60a60b..29e8bccd0 100644
--- a/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt
+++ b/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt
@@ -9,10 +9,13 @@ import com.fredporciuncula.flow.preferences.Preference
import com.fredporciuncula.flow.preferences.Serializer
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R
+import io.github.wulkanowy.data.api.models.Mapping
import io.github.wulkanowy.data.enums.AppTheme
+import io.github.wulkanowy.data.enums.AttendanceCalculatorSortingMode
import io.github.wulkanowy.data.enums.GradeColorTheme
import io.github.wulkanowy.data.enums.GradeExpandMode
import io.github.wulkanowy.data.enums.GradeSortingMode
+import io.github.wulkanowy.data.enums.ShowAdditionalLessonsMode
import io.github.wulkanowy.data.enums.TimetableGapsMode
import io.github.wulkanowy.data.enums.TimetableMode
import io.github.wulkanowy.ui.modules.dashboard.DashboardItem
@@ -41,6 +44,27 @@ class PreferencesRepository @Inject constructor(
R.bool.pref_default_attendance_present
)
+ val targetAttendanceFlow: Flow
+ get() = flowSharedPref.getInt(
+ context.getString(R.string.pref_key_attendance_target),
+ context.resources.getInteger(R.integer.pref_default_attendance_target)
+ ).asFlow()
+
+ val attendanceCalculatorSortingModeFlow: Flow
+ 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
+ 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
get() = getObjectFlow(
R.string.pref_key_grade_average_mode,
@@ -191,6 +215,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(
@@ -346,6 +376,15 @@ class PreferencesRepository @Inject constructor(
get() = sharedPref.getString(PREF_KEY_INSTALLATION_ID, null).orEmpty()
private set(value) = sharedPref.edit { putString(PREF_KEY_INSTALLATION_ID, value) }
+ var mapping: Mapping?
+ get() {
+ val value = sharedPref.getString("mapping", null)
+ return value?.let { json.decodeFromString(it) }
+ }
+ set(value) = sharedPref.edit(commit = true) {
+ putString("mapping", value?.let { json.encodeToString(it) })
+ }
+
init {
if (installationId.isEmpty()) {
installationId = UUID.randomUUID().toString()
diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolAnnouncementRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolAnnouncementRepository.kt
index 6a04ce75f..78d956993 100644
--- a/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolAnnouncementRepository.kt
+++ b/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolAnnouncementRepository.kt
@@ -37,7 +37,7 @@ class SchoolAnnouncementRepository @Inject constructor(
it.isEmpty() || forceRefresh || isExpired
},
query = {
- schoolAnnouncementDb.loadAll(student.userLoginId)
+ schoolAnnouncementDb.loadAll(student.studentId)
},
fetch = {
val sdk = wulkanowySdkFactory.create(student)
@@ -57,7 +57,7 @@ class SchoolAnnouncementRepository @Inject constructor(
)
fun getSchoolAnnouncementFromDatabase(student: Student): Flow> {
- return schoolAnnouncementDb.loadAll(student.userLoginId)
+ return schoolAnnouncementDb.loadAll(student.studentId)
}
suspend fun updateSchoolAnnouncement(schoolAnnouncement: List) =
diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolsRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolsRepository.kt
index 4a16d6f13..25da241a0 100644
--- a/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolsRepository.kt
+++ b/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolsRepository.kt
@@ -1,7 +1,7 @@
package io.github.wulkanowy.data.repositories
import io.github.wulkanowy.data.WulkanowySdkFactory
-import io.github.wulkanowy.data.api.SchoolsService
+import io.github.wulkanowy.data.api.services.SchoolsService
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt
index df47d7a63..575ca89f2 100644
--- a/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt
+++ b/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt
@@ -199,7 +199,7 @@ class StudentRepository @Inject constructor(
suspend fun refreshStudentAfterAuthorize(student: Student, semester: Semester) {
val wulkanowySdk = wulkanowySdkFactory.create(student, semester)
- val newCurrentApiStudent = runCatching { wulkanowySdk.getCurrentStudent() }
+ val newCurrentApiStudent = runCatching { wulkanowySdk.getCurrentStudent() }
.onFailure { Timber.e(it, "Can't find student with id ${student.studentId}") }
.getOrNull() ?: return
diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/TimetableRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/TimetableRepository.kt
index 335789991..60c562e12 100644
--- a/app/src/main/java/io/github/wulkanowy/data/repositories/TimetableRepository.kt
+++ b/app/src/main/java/io/github/wulkanowy/data/repositories/TimetableRepository.kt
@@ -13,6 +13,8 @@ import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.data.networkBoundResource
import io.github.wulkanowy.data.pojos.TimetableFull
import io.github.wulkanowy.services.alarm.TimetableNotificationSchedulerHelper
+import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetProvider
+import io.github.wulkanowy.utils.AppWidgetUpdater
import io.github.wulkanowy.utils.AutoRefreshHelper
import io.github.wulkanowy.utils.getRefreshKey
import io.github.wulkanowy.utils.monday
@@ -26,6 +28,7 @@ import java.time.LocalDate
import javax.inject.Inject
import javax.inject.Singleton
+
@Singleton
class TimetableRepository @Inject constructor(
private val timetableDb: TimetableDao,
@@ -34,6 +37,7 @@ class TimetableRepository @Inject constructor(
private val wulkanowySdkFactory: WulkanowySdkFactory,
private val schedulerHelper: TimetableNotificationSchedulerHelper,
private val refreshHelper: AutoRefreshHelper,
+ private val appWidgetUpdater: AppWidgetUpdater,
) {
private val saveFetchResultMutex = Mutex()
@@ -52,7 +56,8 @@ class TimetableRepository @Inject constructor(
forceRefresh: Boolean,
refreshAdditional: Boolean = false,
notify: Boolean = false,
- timetableType: TimetableType = TimetableType.NORMAL
+ timetableType: TimetableType = TimetableType.NORMAL,
+ isFromAppWidget: Boolean = false
) = networkBoundResource(
mutex = saveFetchResultMutex,
isResultEmpty = {
@@ -83,6 +88,9 @@ class TimetableRepository @Inject constructor(
refreshDayHeaders(timetableOld.headers, timetableNew.headers)
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester, start, end))
+ if (!isFromAppWidget) {
+ appWidgetUpdater.updateAllAppWidgetsByProvider(TimetableWidgetProvider::class)
+ }
},
filterResult = { (timetable, additional, headers) ->
TimetableFull(
diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/WulkanowyRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/WulkanowyRepository.kt
new file mode 100644
index 000000000..815f8b758
--- /dev/null
+++ b/app/src/main/java/io/github/wulkanowy/data/repositories/WulkanowyRepository.kt
@@ -0,0 +1,66 @@
+package io.github.wulkanowy.data.repositories
+
+import io.github.wulkanowy.data.Resource
+import io.github.wulkanowy.data.api.models.Mapping
+import io.github.wulkanowy.data.api.services.WulkanowyService
+import io.github.wulkanowy.data.db.dao.AdminMessageDao
+import io.github.wulkanowy.data.db.entities.AdminMessage
+import io.github.wulkanowy.data.networkBoundResource
+import io.github.wulkanowy.utils.AutoRefreshHelper
+import io.github.wulkanowy.utils.getRefreshKey
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.filterNot
+import kotlinx.coroutines.sync.Mutex
+import timber.log.Timber
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class WulkanowyRepository @Inject constructor(
+ private val wulkanowyService: WulkanowyService,
+ private val adminMessageDao: AdminMessageDao,
+ private val preferencesRepository: PreferencesRepository,
+ private val refreshHelper: AutoRefreshHelper,
+) {
+
+ private val saveFetchResultMutex = Mutex()
+
+ private val cacheKey = "mapping_refresh_key"
+
+ fun getAdminMessages(): Flow>> =
+ networkBoundResource(
+ mutex = saveFetchResultMutex,
+ isResultEmpty = { false },
+ query = { adminMessageDao.loadAll() },
+ fetch = { wulkanowyService.getAdminMessages() },
+ shouldFetch = { true },
+ saveFetchResult = { oldItems, newItems ->
+ adminMessageDao.removeOldAndSaveNew(oldItems, newItems)
+ },
+ )
+ .filterNot { it is Resource.Intermediate }
+
+ suspend fun getMapping(): Mapping? {
+ var savedMapping = preferencesRepository.mapping
+
+ val isExpired = refreshHelper.shouldBeRefreshed(
+ key = getRefreshKey(cacheKey)
+ )
+
+ if (savedMapping == null || isExpired) {
+ fetchMapping()
+ savedMapping = preferencesRepository.mapping
+ }
+
+ return savedMapping
+ }
+
+ suspend fun fetchMapping() {
+ runCatching { wulkanowyService.getMapping() }
+ .onFailure { Timber.e(it) }
+ .onSuccess {
+ preferencesRepository.mapping = it
+ refreshHelper.updateLastRefreshTimestamp(cacheKey)
+ }
+ }
+}
diff --git a/app/src/main/java/io/github/wulkanowy/data/serializers/SafeMessageTypeEnumListSerializer.kt b/app/src/main/java/io/github/wulkanowy/data/serializers/SafeMessageTypeEnumListSerializer.kt
new file mode 100644
index 000000000..a95eab807
--- /dev/null
+++ b/app/src/main/java/io/github/wulkanowy/data/serializers/SafeMessageTypeEnumListSerializer.kt
@@ -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> {
+
+ private val serializer = ListSerializer(String.serializer())
+
+ override val descriptor = serializer.descriptor
+
+ override fun serialize(encoder: Encoder, value: List) {
+ encoder.encodeNotNullMark()
+ serializer.serialize(encoder, value.map { it.name })
+ }
+
+ override fun deserialize(decoder: Decoder): List =
+ serializer.deserialize(decoder).mapNotNull { enumName ->
+ MessageType.entries.find { it.name == enumName }
+ }
+}
diff --git a/app/src/main/java/io/github/wulkanowy/domain/adminmessage/GetAppropriateAdminMessageUseCase.kt b/app/src/main/java/io/github/wulkanowy/domain/adminmessage/GetAppropriateAdminMessageUseCase.kt
index b55bf899d..8b0d67b57 100644
--- a/app/src/main/java/io/github/wulkanowy/domain/adminmessage/GetAppropriateAdminMessageUseCase.kt
+++ b/app/src/main/java/io/github/wulkanowy/domain/adminmessage/GetAppropriateAdminMessageUseCase.kt
@@ -5,14 +5,14 @@ import io.github.wulkanowy.data.db.entities.AdminMessage
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.enums.MessageType
import io.github.wulkanowy.data.mapResourceData
-import io.github.wulkanowy.data.repositories.AdminMessageRepository
import io.github.wulkanowy.data.repositories.PreferencesRepository
+import io.github.wulkanowy.data.repositories.WulkanowyRepository
import io.github.wulkanowy.utils.AppInfo
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
class GetAppropriateAdminMessageUseCase @Inject constructor(
- private val adminMessageRepository: AdminMessageRepository,
+ private val wulkanowyRepository: WulkanowyRepository,
private val preferencesRepository: PreferencesRepository,
private val appInfo: AppInfo
) {
@@ -22,7 +22,7 @@ class GetAppropriateAdminMessageUseCase @Inject constructor(
}
operator fun invoke(scrapperBaseUrl: String, type: MessageType): Flow> {
- return adminMessageRepository.getAdminMessages().mapResourceData { adminMessages ->
+ return wulkanowyRepository.getAdminMessages().mapResourceData { adminMessages ->
adminMessages
.asSequence()
.filter { it.isNotDismissed() }
diff --git a/app/src/main/java/io/github/wulkanowy/domain/attendance/GetAttendanceCalculatorDataUseCase.kt b/app/src/main/java/io/github/wulkanowy/domain/attendance/GetAttendanceCalculatorDataUseCase.kt
new file mode 100644
index 000000000..294abd1be
--- /dev/null
+++ b/app/src/main/java/io/github/wulkanowy/domain/attendance/GetAttendanceCalculatorDataUseCase.kt
@@ -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>> =
+ 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::sortedBy)
+}
+
+private fun List.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.sortedBy(mode: AttendanceCalculatorSortingMode) = when (mode) {
+ ALPHABETIC -> sortedBy(AttendanceData::subjectName)
+ ATTENDANCE -> sortedByDescending(AttendanceData::presencePercentage)
+ LESSON_BALANCE -> sortedBy(AttendanceData::lessonBalance)
+}
diff --git a/app/src/main/java/io/github/wulkanowy/domain/messages/GetMailboxByStudentUseCase.kt b/app/src/main/java/io/github/wulkanowy/domain/messages/GetMailboxByStudentUseCase.kt
index 669514aae..8f25eba14 100644
--- a/app/src/main/java/io/github/wulkanowy/domain/messages/GetMailboxByStudentUseCase.kt
+++ b/app/src/main/java/io/github/wulkanowy/domain/messages/GetMailboxByStudentUseCase.kt
@@ -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))
}
}
}
diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceFragment.kt
index 6e842b4d7..07649e436 100644
--- a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceFragment.kt
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceFragment.kt
@@ -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(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(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)
}
diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendancePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendancePresenter.kt
index 82fe69cb7..586a41ad0 100644
--- a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendancePresenter.kt
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendancePresenter.kt
@@ -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")
diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceView.kt
index 2629c217e..f51ce7c7e 100644
--- a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceView.kt
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceView.kt
@@ -56,6 +56,8 @@ interface AttendanceView : BaseView {
fun openSummaryView()
+ fun openCalculatorView()
+
fun startSendMessageIntent(date: LocalDate, numbers: String, reason: String)
fun startActionMode()
diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/calculator/AttendanceCalculatorAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/calculator/AttendanceCalculatorAdapter.kt
new file mode 100644
index 000000000..4b908bba8
--- /dev/null
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/calculator/AttendanceCalculatorAdapter.kt
@@ -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() {
+
+ var items = emptyList()
+
+ 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)
+}
diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/calculator/AttendanceCalculatorFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/calculator/AttendanceCalculatorFragment.kt
new file mode 100644
index 000000000..63d1d8be5
--- /dev/null
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/calculator/AttendanceCalculatorFragment.kt
@@ -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(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) {
+ 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()
+ }
+}
diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/calculator/AttendanceCalculatorPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/calculator/AttendanceCalculatorPresenter.kt
new file mode 100644
index 000000000..29cb2197f
--- /dev/null
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/calculator/AttendanceCalculatorPresenter.kt
@@ -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(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
+ }
+}
diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/calculator/AttendanceCalculatorView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/calculator/AttendanceCalculatorView.kt
new file mode 100644
index 000000000..21afe532e
--- /dev/null
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/calculator/AttendanceCalculatorView.kt
@@ -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)
+
+ fun clearView()
+
+ fun openSettingsView()
+}
diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/auth/AuthPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/auth/AuthPresenter.kt
index 5c597eeb4..0be086b69 100644
--- a/app/src/main/java/io/github/wulkanowy/ui/modules/auth/AuthPresenter.kt
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/auth/AuthPresenter.kt
@@ -27,8 +27,12 @@ class AuthPresenter @Inject constructor(
private fun loadName() {
presenterScope.launch {
- runCatching { studentRepository.getCurrentStudent(false) }
- .onSuccess { view?.showDescriptionWithName(it.studentName) }
+ runCatching {
+ studentRepository.getCurrentStudent(false)
+ .studentName
+ .replace(" ", "\u00A0")
+ }
+ .onSuccess { view?.showDescriptionWithName(it) }
.onFailure { errorHandler.dispatch(it) }
}
}
diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/captcha/CaptchaDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/captcha/CaptchaDialog.kt
index ce2173d28..922b652be 100644
--- a/app/src/main/java/io/github/wulkanowy/ui/modules/captcha/CaptchaDialog.kt
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/captcha/CaptchaDialog.kt
@@ -59,7 +59,7 @@ class CaptchaDialog : BaseDialogFragment() {
webView = this
with(settings) {
javaScriptEnabled = true
- userAgentString = wulkanowySdkFactory.create().userAgent
+ userAgentString = wulkanowySdkFactory.createBase().userAgent
}
webViewClient = object : WebViewClient() {
diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardFragment.kt
index b7a0796c5..25774d044 100644
--- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardFragment.kt
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardFragment.kt
@@ -30,6 +30,7 @@ import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.ui.modules.message.MessageFragment
import io.github.wulkanowy.ui.modules.notificationscenter.NotificationsCenterFragment
+import io.github.wulkanowy.ui.modules.panicmode.PanicModeFragment
import io.github.wulkanowy.ui.modules.schoolannouncement.SchoolAnnouncementFragment
import io.github.wulkanowy.ui.modules.timetable.TimetableFragment
import io.github.wulkanowy.utils.capitalise
@@ -125,6 +126,7 @@ class DashboardFragment : BaseFragment(R.layout.fragme
mainActivity.pushView(ConferenceFragment.newInstance())
}
onAdminMessageClickListener = presenter::onAdminMessageSelected
+ onPanicButtonClickListener = presenter::onPanicButtonClicked
onAdminMessageDismissClickListener = presenter::onAdminMessageDismissed
registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
@@ -208,7 +210,11 @@ class DashboardFragment : BaseFragment(R.layout.fragme
binding = binding.dashboardErrorAdminMessage,
onAdminMessageDismissClickListener = presenter::onAdminMessageDismissed,
onAdminMessageClickListener = presenter::onAdminMessageSelected,
- ).bind(adminMessageItem.adminMessage)
+ onPanicButtonClickListener = presenter::onPanicButtonClicked,
+ ).bind(
+ item = adminMessageItem.adminMessage,
+ showPanicButton = true,
+ )
}
}
@@ -236,6 +242,10 @@ class DashboardFragment : BaseFragment(R.layout.fragme
requireContext().openInternetBrowser(url)
}
+ override fun openPanicWebView(url: String) {
+ (requireActivity() as MainActivity).pushView(PanicModeFragment.newInstance(url))
+ }
+
override fun onDestroyView() {
dashboardAdapter.clearTimers()
presenter.onDetachView()
diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt
index 3fec62562..a70f82e20 100644
--- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt
@@ -11,6 +11,7 @@ import io.github.wulkanowy.data.errorOrNull
import io.github.wulkanowy.data.flatResourceFlow
import io.github.wulkanowy.data.mapResourceData
import io.github.wulkanowy.data.onResourceError
+import io.github.wulkanowy.data.onResourceSuccess
import io.github.wulkanowy.data.repositories.AttendanceSummaryRepository
import io.github.wulkanowy.data.repositories.ConferenceRepository
import io.github.wulkanowy.data.repositories.ExamRepository
@@ -23,6 +24,7 @@ import io.github.wulkanowy.data.repositories.SchoolAnnouncementRepository
import io.github.wulkanowy.data.repositories.SemesterRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.repositories.TimetableRepository
+import io.github.wulkanowy.data.resourceFlow
import io.github.wulkanowy.domain.adminmessage.GetAppropriateAdminMessageUseCase
import io.github.wulkanowy.domain.timetable.IsStudentHasLessonsOnWeekendUseCase
import io.github.wulkanowy.ui.base.BasePresenter
@@ -44,6 +46,7 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
+import okhttp3.HttpUrl.Companion.toHttpUrl
import timber.log.Timber
import java.time.Instant
import java.time.LocalDate
@@ -282,6 +285,22 @@ class DashboardPresenter @Inject constructor(
url?.let { view?.openInternetBrowser(it) }
}
+ fun onPanicButtonClicked() {
+ resourceFlow { studentRepository.getCurrentStudent() }
+ .onResourceError { errorHandler.dispatch(it) }
+ .onResourceSuccess {
+ val baseUrl = it.scrapperBaseUrl.toHttpUrl()
+ val urlToOpen = baseUrl.newBuilder()
+ .host("uonetplus${it.scrapperDomainSuffix}.${baseUrl.host}")
+ .addPathSegment(it.symbol)
+ .build()
+ .toString()
+
+ view?.openPanicWebView(urlToOpen)
+ }
+ .launch("panic_button")
+ }
+
private fun loadHorizontalGroup(student: Student, forceRefresh: Boolean) {
flow {
val selectedTiles = selectedDashboardTiles
diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardView.kt
index 56a0a773a..dd7fb962f 100644
--- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardView.kt
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardView.kt
@@ -31,4 +31,6 @@ interface DashboardView : BaseView {
fun openNotificationsCenterView()
fun openInternetBrowser(url: String)
+
+ fun openPanicWebView(url: String)
}
diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/adapters/DashboardAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/adapters/DashboardAdapter.kt
index 7c74cae80..e51975701 100644
--- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/adapters/DashboardAdapter.kt
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/adapters/DashboardAdapter.kt
@@ -59,6 +59,8 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter Unit = {}
+ var onPanicButtonClickListener: () -> Unit = {}
+
var onAdminMessageDismissClickListener: (AdminMessage) -> Unit = {}
val items = mutableListOf()
@@ -86,35 +88,46 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter AccountViewHolder(
ItemDashboardAccountBinding.inflate(inflater, parent, false)
)
+
DashboardItem.Type.HORIZONTAL_GROUP.ordinal -> HorizontalGroupViewHolder(
ItemDashboardHorizontalGroupBinding.inflate(inflater, parent, false)
)
+
DashboardItem.Type.GRADES.ordinal -> GradesViewHolder(
ItemDashboardGradesBinding.inflate(inflater, parent, false)
)
+
DashboardItem.Type.LESSONS.ordinal -> LessonsViewHolder(
ItemDashboardLessonsBinding.inflate(inflater, parent, false)
)
+
DashboardItem.Type.HOMEWORK.ordinal -> HomeworkViewHolder(
ItemDashboardHomeworkBinding.inflate(inflater, parent, false)
)
+
DashboardItem.Type.ANNOUNCEMENTS.ordinal -> AnnouncementsViewHolder(
ItemDashboardAnnouncementsBinding.inflate(inflater, parent, false)
)
+
DashboardItem.Type.EXAMS.ordinal -> ExamsViewHolder(
ItemDashboardExamsBinding.inflate(inflater, parent, false)
)
+
DashboardItem.Type.CONFERENCES.ordinal -> ConferencesViewHolder(
ItemDashboardConferencesBinding.inflate(inflater, parent, false)
)
+
DashboardItem.Type.ADMIN_MESSAGE.ordinal -> AdminMessageViewHolder(
ItemDashboardAdminMessageBinding.inflate(inflater, parent, false),
onAdminMessageDismissClickListener = onAdminMessageDismissClickListener,
onAdminMessageClickListener = onAdminMessageClickListener,
+ onPanicButtonClickListener = onPanicButtonClickListener,
)
+
DashboardItem.Type.ADS.ordinal -> AdsViewHolder(
ItemDashboardAdsBinding.inflate(inflater, parent, false)
)
+
else -> throw IllegalArgumentException()
}
}
@@ -129,7 +142,11 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter bindAnnouncementsViewHolder(holder, position)
is ExamsViewHolder -> bindExamsViewHolder(holder, position)
is ConferencesViewHolder -> bindConferencesViewHolder(holder, position)
- is AdminMessageViewHolder -> holder.bind((items[position] as DashboardItem.AdminMessages).adminMessage)
+ is AdminMessageViewHolder -> holder.bind(
+ (items[position] as DashboardItem.AdminMessages).adminMessage,
+ showPanicButton = true
+ )
+
is AdsViewHolder -> bindAdsViewHolder(holder, position)
}
}
@@ -240,12 +257,15 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter {
root.context.getThemeAttrColor(R.attr.colorOnSurface)
}
+
attendancePercentage <= ATTENDANCE_SECOND_WARNING_THRESHOLD -> {
root.context.getThemeAttrColor(R.attr.colorPrimary)
}
+
attendancePercentage <= ATTENDANCE_FIRST_WARNING_THRESHOLD -> {
root.context.getThemeAttrColor(R.attr.colorTimetableChange)
}
+
else -> root.context.getThemeAttrColor(R.attr.colorOnSurface)
}
val attendanceString = if (attendancePercentage == null || attendancePercentage == .0) {
@@ -336,24 +356,28 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter {
dateToNavigate = tomorrowDate
updateLessonView(item, tomorrowTimetable, binding)
binding.dashboardLessonsItemTitleTomorrow.isVisible = true
binding.dashboardLessonsItemTitleTodayAndTomorrow.isVisible = false
}
+
currentDayHeader != null && currentDayHeader.content.isNotBlank() -> {
dateToNavigate = currentDate
updateLessonView(item, emptyList(), binding, currentDayHeader)
binding.dashboardLessonsItemTitleTomorrow.isVisible = false
binding.dashboardLessonsItemTitleTodayAndTomorrow.isVisible = false
}
+
tomorrowDayHeader != null && tomorrowDayHeader.content.isNotBlank() -> {
dateToNavigate = tomorrowDate
updateLessonView(item, emptyList(), binding, tomorrowDayHeader)
binding.dashboardLessonsItemTitleTomorrow.isVisible = true
binding.dashboardLessonsItemTitleTodayAndTomorrow.isVisible = false
}
+
else -> {
dateToNavigate = currentDate
updateLessonView(item, emptyList(), binding)
@@ -461,6 +485,7 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter {
firstTitleAndValueTextColor =
context.getThemeAttrColor(R.attr.colorOnSurface)
@@ -468,6 +493,7 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter {
firstTitleAndValueTextColor =
context.getThemeAttrColor(R.attr.colorOnSurface)
diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/viewholders/AdminMessageViewHolder.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/viewholders/AdminMessageViewHolder.kt
index 1e0f0bdbf..94a7686db 100644
--- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/viewholders/AdminMessageViewHolder.kt
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/viewholders/AdminMessageViewHolder.kt
@@ -13,9 +13,10 @@ class AdminMessageViewHolder(
private val binding: ItemDashboardAdminMessageBinding,
private val onAdminMessageDismissClickListener: (AdminMessage) -> Unit,
private val onAdminMessageClickListener: (String?) -> Unit,
+ private val onPanicButtonClickListener: () -> Unit,
) : RecyclerView.ViewHolder(binding.root) {
- fun bind(item: AdminMessage?) {
+ fun bind(item: AdminMessage?, showPanicButton: Boolean = false) {
item ?: return
val context = binding.root.context
@@ -48,10 +49,14 @@ class AdminMessageViewHolder(
dashboardAdminMessageItemClose.setOnClickListener {
onAdminMessageDismissClickListener(item)
}
+ dashboardPanicSection.root.isVisible = showPanicButton
+ dashboardPanicSection.dashboardPanicButton.setOnClickListener {
+ onPanicButtonClickListener()
+ }
- root.setCardBackgroundColor(backgroundColor?.let { ColorStateList.valueOf(it) })
+ dashboardAdminMessage.setCardBackgroundColor(backgroundColor?.let { ColorStateList.valueOf(it) })
item.destinationUrl?.let { url ->
- root.setOnClickListener { onAdminMessageClickListener(url) }
+ dashboardAdminMessage.setOnClickListener { onAdminMessageClickListener(url) }
}
}
}
diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/mock/gradeSummary.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/mock/gradeSummary.kt
index c452204b9..e92d1afb3 100644
--- a/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/mock/gradeSummary.kt
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/mock/gradeSummary.kt
@@ -26,5 +26,7 @@ private fun generateSummary(subject: String, predicted: String, final: String) =
proposedPoints = "",
finalPoints = "",
pointsSum = "",
- average = .0
+ average = .0,
+ pointsSumAllYear = null,
+ averageAllYear = null,
)
diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/mock/schoolAnnouncement.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/mock/schoolAnnouncement.kt
index e2dc5cd84..9b21f08e6 100644
--- a/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/mock/schoolAnnouncement.kt
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/mock/schoolAnnouncement.kt
@@ -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()
)
diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProvider.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProvider.kt
index 8da59eaf4..7f14c01f1 100644
--- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProvider.kt
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProvider.kt
@@ -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,
)
}
}
diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsAdapter.kt
index 15b5db031..bcbd2df2f 100644
--- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsAdapter.kt
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsAdapter.kt
@@ -96,9 +96,11 @@ class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter 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 bindItemViewHolder(
holder = holder,
grade = items[position].value as Grade
@@ -133,6 +136,10 @@ class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter
) {
diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsPresenter.kt
index d9621f51e..ec5d34c5e 100644
--- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsPresenter.kt
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsPresenter.kt
@@ -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
diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryAdapter.kt
index 95cf97bed..1cc74ef09 100644
--- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryAdapter.kt
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryAdapter.kt
@@ -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
}
}
diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginActivity.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginActivity.kt
index 88f295788..e528c5147 100644
--- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginActivity.kt
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginActivity.kt
@@ -118,5 +118,6 @@ class LoginActivity : BaseActivity(), Logi
override fun onResume() {
super.onResume()
inAppUpdateHelper.onResume()
+ presenter.updateSdkMappings()
}
}
diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginPresenter.kt
index 9031cb8ab..aff0515f0 100644
--- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginPresenter.kt
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginPresenter.kt
@@ -1,12 +1,15 @@
package io.github.wulkanowy.ui.modules.login
import io.github.wulkanowy.data.repositories.StudentRepository
+import io.github.wulkanowy.data.repositories.WulkanowyRepository
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
+import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
class LoginPresenter @Inject constructor(
+ private val wulkanowyRepository: WulkanowyRepository,
errorHandler: ErrorHandler,
studentRepository: StudentRepository
) : BasePresenter(errorHandler, studentRepository) {
@@ -16,4 +19,11 @@ class LoginPresenter @Inject constructor(
view.initView()
Timber.i("Login view was initialized")
}
+
+ fun updateSdkMappings() {
+ presenterScope.launch {
+ runCatching { wulkanowyRepository.fetchMapping() }
+ .onFailure { Timber.e(it) }
+ }
+ }
}
diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt
index 1c4920696..2d1a48243 100644
--- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt
@@ -238,6 +238,7 @@ class LoginFormFragment : BaseFragment(R.layout.fragme
binding = binding.loginFormMessage,
onAdminMessageDismissClickListener = presenter::onAdminMessageDismissed,
onAdminMessageClickListener = presenter::onAdminMessageSelected,
+ onPanicButtonClickListener = {},
).bind(message)
binding.loginFormMessage.root.isVisible = message != null
}
diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectAdapter.kt
index e6d131829..ef8cf4ee9 100644
--- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectAdapter.kt
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectAdapter.kt
@@ -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
}
diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectFragment.kt
index 0fe36aa99..586c395c3 100644
--- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectFragment.kt
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectFragment.kt
@@ -6,10 +6,12 @@ import androidx.core.os.bundleOf
import androidx.core.view.isVisible
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
+import io.github.wulkanowy.data.db.entities.AdminMessage
import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.databinding.FragmentLoginStudentSelectBinding
import io.github.wulkanowy.ui.base.BaseFragment
+import io.github.wulkanowy.ui.modules.dashboard.viewholders.AdminMessageViewHolder
import io.github.wulkanowy.ui.modules.login.LoginActivity
import io.github.wulkanowy.ui.modules.login.LoginData
import io.github.wulkanowy.ui.modules.login.support.LoginSupportDialog
@@ -111,6 +113,20 @@ class LoginStudentSelectFragment :
LoginSupportDialog.newInstance(supportInfo).show(childFragmentManager, "support_dialog")
}
+ override fun showAdminMessage(adminMessage: AdminMessage?) {
+ AdminMessageViewHolder(
+ binding = binding.loginStudentSelectAdminMessage,
+ onAdminMessageDismissClickListener = presenter::onAdminMessageDismissed,
+ onAdminMessageClickListener = presenter::onAdminMessageSelected,
+ onPanicButtonClickListener = {},
+ ).bind(adminMessage)
+ binding.loginStudentSelectAdminMessage.root.isVisible = adminMessage != null
+ }
+
+ override fun openInternetBrowser(url: String) {
+ requireContext().openInternetBrowser(url)
+ }
+
override fun onDestroyView() {
presenter.onDetachView()
super.onDestroyView()
diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenter.kt
index 344414180..39070cf0a 100644
--- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenter.kt
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenter.kt
@@ -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(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 {
@@ -108,8 +132,8 @@ class LoginStudentSelectPresenter @Inject constructor(
}
private fun createItems(): List = 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,
students: List,
@@ -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)
+ }
}
diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectView.kt
index b69700f17..4d0ef9e92 100644
--- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectView.kt
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectView.kt
@@ -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)
}
diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolFragment.kt
index 23ebffe9d..824d8d22c 100644
--- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolFragment.kt
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolFragment.kt
@@ -9,13 +9,16 @@ import android.view.inputmethod.EditorInfo.IME_NULL
import android.widget.ArrayAdapter
import androidx.core.os.bundleOf
import androidx.core.text.parseAsHtml
+import androidx.core.view.isVisible
import androidx.core.widget.doOnTextChanged
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
+import io.github.wulkanowy.data.db.entities.AdminMessage
import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.databinding.FragmentLoginSymbolBinding
import io.github.wulkanowy.ui.base.BaseFragment
+import io.github.wulkanowy.ui.modules.dashboard.viewholders.AdminMessageViewHolder
import io.github.wulkanowy.ui.modules.login.LoginActivity
import io.github.wulkanowy.ui.modules.login.LoginData
import io.github.wulkanowy.ui.modules.login.support.LoginSupportDialog
@@ -179,4 +182,18 @@ class LoginSymbolFragment :
override fun openSupportDialog(supportInfo: LoginSupportInfo) {
LoginSupportDialog.newInstance(supportInfo).show(childFragmentManager, "support_dialog")
}
+
+ override fun showAdminMessage(adminMessage: AdminMessage?) {
+ AdminMessageViewHolder(
+ binding = binding.loginSymbolAdminMessage,
+ onAdminMessageDismissClickListener = presenter::onAdminMessageDismissed,
+ onAdminMessageClickListener = presenter::onAdminMessageSelected,
+ onPanicButtonClickListener = {},
+ ).bind(adminMessage)
+ binding.loginSymbolAdminMessage.root.isVisible = adminMessage != null
+ }
+
+ override fun openInternetBrowser(url: String) {
+ requireContext().openInternetBrowser(url)
+ }
}
diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolPresenter.kt
index 5c31f14d4..8de2994a7 100644
--- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolPresenter.kt
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolPresenter.kt
@@ -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(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)
+ }
}
diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolView.kt
index ace12f780..2fc910242 100644
--- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolView.kt
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolView.kt
@@ -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)
}
diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/history/LuckyNumberHistoryAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/history/LuckyNumberHistoryAdapter.kt
index 0c1b89c8e..9c718af45 100644
--- a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/history/LuckyNumberHistoryAdapter.kt
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/history/LuckyNumberHistoryAdapter.kt
@@ -33,4 +33,4 @@ class LuckyNumberHistoryAdapter @Inject constructor() :
}
class ItemViewHolder(val binding: ItemLuckyNumberHistoryBinding) : RecyclerView.ViewHolder(binding.root)
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetProvider.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetProvider.kt
index 1ab079a3a..e6de17818 100644
--- a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetProvider.kt
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetProvider.kt
@@ -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
diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt
index 62c16257e..e64aa9b07 100644
--- a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt
@@ -138,6 +138,7 @@ class MainActivity : BaseActivity(), MainVie
override fun onResume() {
super.onResume()
inAppUpdateHelper.onResume()
+ presenter.updateSdkMappings()
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainPresenter.kt
index a544381ce..6a072718d 100644
--- a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainPresenter.kt
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainPresenter.kt
@@ -6,6 +6,7 @@ import io.github.wulkanowy.data.onResourceError
import io.github.wulkanowy.data.onResourceSuccess
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.StudentRepository
+import io.github.wulkanowy.data.repositories.WulkanowyRepository
import io.github.wulkanowy.data.resourceFlow
import io.github.wulkanowy.services.sync.SyncManager
import io.github.wulkanowy.ui.base.BasePresenter
@@ -29,6 +30,7 @@ class MainPresenter @Inject constructor(
errorHandler: ErrorHandler,
studentRepository: StudentRepository,
private val preferencesRepository: PreferencesRepository,
+ private val wulkanowyRepository: WulkanowyRepository,
private val syncManager: SyncManager,
private val analytics: AnalyticsHelper,
private val json: Json,
@@ -199,4 +201,11 @@ class MainPresenter @Inject constructor(
.onFailure { errorHandler.dispatch(it) }
}
}
+
+ fun updateSdkMappings() {
+ presenterScope.launch {
+ runCatching { wulkanowyRepository.fetchMapping() }
+ .onFailure { Timber.e(it) }
+ }
+ }
}
diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabFragment.kt
index 12f9d3234..b29c2cd85 100644
--- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabFragment.kt
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabFragment.kt
@@ -27,6 +27,7 @@ import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.message.MessageFragment
import io.github.wulkanowy.ui.modules.message.mailboxchooser.MailboxChooserDialog
import io.github.wulkanowy.ui.modules.message.preview.MessagePreviewFragment
+import io.github.wulkanowy.ui.modules.panicmode.PanicModeFragment
import io.github.wulkanowy.ui.widgets.DividerItemDecoration
import io.github.wulkanowy.utils.dpToPx
import io.github.wulkanowy.utils.getThemeAttrColor
@@ -132,6 +133,7 @@ class MessageTabFragment : BaseFragment(R.layout.frag
)
messageTabErrorRetry.setOnClickListener { presenter.onRetry() }
messageTabErrorDetails.setOnClickListener { presenter.onDetailsClick() }
+ messageTabPanicSection.dashboardPanicButton.setOnClickListener { presenter.onPanicButtonClicked() }
}
setFragmentResultListener(requireArguments().getString(MESSAGE_TAB_FOLDER_ID)!!) { _, bundle ->
@@ -283,6 +285,10 @@ class MessageTabFragment : BaseFragment(R.layout.frag
)
}
+ override fun openPanicWebView(url: String) {
+ (requireActivity() as MainActivity).pushView(PanicModeFragment.newInstance(url))
+ }
+
override fun hideKeyboard() {
activity?.hideSoftInput()
}
diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabPresenter.kt
index cda0b32bd..ad3e7c9cd 100644
--- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabPresenter.kt
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabPresenter.kt
@@ -20,6 +20,7 @@ import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import me.xdrop.fuzzywuzzy.FuzzySearch
+import okhttp3.HttpUrl.Companion.toHttpUrl
import timber.log.Timber
import javax.inject.Inject
import kotlin.math.pow
@@ -429,4 +430,20 @@ class MessageTabPresenter @Inject constructor(
+ dateRatio.toDouble().pow(2) * 2
).toInt()
}
+
+ fun onPanicButtonClicked() {
+ resourceFlow { studentRepository.getCurrentStudent() }
+ .onResourceError { errorHandler.dispatch(it) }
+ .onResourceSuccess {
+ val baseUrl = it.scrapperBaseUrl.toHttpUrl()
+ val urlToOpen = baseUrl.newBuilder()
+ .host("uonetplus${it.scrapperDomainSuffix}-wiadomosciplus.${baseUrl.host}")
+ .addPathSegment(it.symbol)
+ .build()
+ .toString()
+
+ view?.openPanicWebView(urlToOpen)
+ }
+ .launch("panic_button")
+ }
}
diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabView.kt
index 247af4342..e544a3d68 100644
--- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabView.kt
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabView.kt
@@ -50,4 +50,6 @@ interface MessageTabView : BaseView {
fun showRecyclerBottomPadding(show: Boolean)
fun showMailboxChooser(mailboxes: List)
+
+ fun openPanicWebView(url: String)
}
diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/panicmode/PanicModeFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/panicmode/PanicModeFragment.kt
new file mode 100644
index 000000000..df51c5f94
--- /dev/null
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/panicmode/PanicModeFragment.kt
@@ -0,0 +1,99 @@
+package io.github.wulkanowy.ui.modules.panicmode
+
+import android.annotation.SuppressLint
+import android.os.Bundle
+import android.view.View
+import android.webkit.WebView
+import android.webkit.WebViewClient
+import androidx.activity.addCallback
+import androidx.core.os.bundleOf
+import dagger.hilt.android.AndroidEntryPoint
+import io.github.wulkanowy.R
+import io.github.wulkanowy.data.WulkanowySdkFactory
+import io.github.wulkanowy.databinding.FragmentPanicModeBinding
+import io.github.wulkanowy.ui.base.BaseFragment
+import io.github.wulkanowy.ui.modules.main.MainView
+import io.github.wulkanowy.utils.WebkitCookieManagerProxy
+import io.github.wulkanowy.utils.openInternetBrowser
+import javax.inject.Inject
+
+@AndroidEntryPoint
+class PanicModeFragment : BaseFragment(R.layout.fragment_panic_mode),
+ MainView.TitledView {
+
+ @Inject
+ lateinit var wulkanowySdkFactory: WulkanowySdkFactory
+
+ @Inject
+ lateinit var webkitCookieManagerProxy: WebkitCookieManagerProxy
+
+ private var webView: WebView? = null
+
+ override val titleStringId: Int get() = R.string.panic_mode_title
+
+ companion object {
+
+ private const val PANIC_URL = "panic_mode_url"
+ fun newInstance(url: String?): PanicModeFragment {
+ return PanicModeFragment().apply {
+ arguments = bundleOf(PANIC_URL to url)
+ }
+ }
+ }
+
+ @SuppressLint("SetJavaScriptEnabled")
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ binding = FragmentPanicModeBinding.bind(view)
+
+ binding.panicModeRefresh.setOnClickListener {
+ binding.panicModeWebview.loadUrl(
+ binding.panicModeWebview.url ?: arguments?.getString(PANIC_URL).orEmpty()
+ )
+ }
+ binding.panicModeBack.setOnClickListener { binding.panicModeWebview.goBack() }
+ binding.panicModeHome.setOnClickListener {
+ binding.panicModeWebview.loadUrl(
+ arguments?.getString(PANIC_URL).orEmpty()
+ )
+ }
+ binding.panicModeForward.setOnClickListener { binding.panicModeWebview.goForward() }
+ binding.panicModeShare.setOnClickListener {
+ requireContext().openInternetBrowser(
+ binding.panicModeWebview.url.toString(),
+ )
+ }
+
+ val onBackPressedCallback = requireActivity().onBackPressedDispatcher
+ .addCallback(viewLifecycleOwner) {
+ binding.panicModeWebview.goBack()
+ }
+
+ with(binding.panicModeWebview) {
+ webView = this
+ with(settings) {
+ javaScriptEnabled = true
+ userAgentString = wulkanowySdkFactory.createBase().userAgent
+ }
+
+ webViewClient = object : WebViewClient() {
+ override fun doUpdateVisitedHistory(
+ view: WebView?,
+ url: String?,
+ isReload: Boolean
+ ) {
+ binding.panicModeBack.isEnabled = binding.panicModeWebview.canGoBack()
+ binding.panicModeForward.isEnabled = binding.panicModeWebview.canGoForward()
+ onBackPressedCallback.isEnabled = binding.panicModeWebview.canGoBack()
+ }
+ }
+ loadUrl(arguments?.getString(PANIC_URL).orEmpty())
+ }
+ }
+
+ override fun onDestroy() {
+ webkitCookieManagerProxy.webkitCookieManager?.flush()
+ webView?.destroy()
+ super.onDestroy()
+ }
+}
diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/AppearanceFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/AppearanceFragment.kt
index 3d0c8052b..62544f83e 100644
--- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/AppearanceFragment.kt
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/AppearanceFragment.kt
@@ -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(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?) {
diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableAdapter.kt
index a4221a2a2..5cb6c401f 100644
--- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableAdapter.kt
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableAdapter.kt
@@ -7,20 +7,21 @@ 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(differ) {
+ SyncListAdapter(Differ) {
override fun getItemViewType(position: Int): Int = getItem(position).type.ordinal
@@ -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")
}
}
@@ -307,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() {
- 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() {
+ 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)
}
}
}
diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableFragment.kt
index 0e6459110..b73e7c26d 100644
--- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableFragment.kt
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableFragment.kt
@@ -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(R.layout.fragme
}
}
- override fun updateData(data: List) {
- timetableAdapter.submitList(data)
+ override fun updateData(data: List, isDayChanged: Boolean) {
+ when {
+ isDayChanged -> timetableAdapter.recreate(data)
+ else -> timetableAdapter.submitList(data)
+ }
}
override fun clearData() {
diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableItem.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableItem.kt
index 402b03dd9..93290ba21 100644
--- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableItem.kt
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableItem.kt
@@ -1,6 +1,7 @@
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) {
@@ -23,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(
@@ -34,5 +39,6 @@ data class TimeLeft(
enum class TimetableItemType {
SMALL,
NORMAL,
- EMPTY
+ EMPTY,
+ ADDITIONAL,
}
diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetablePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetablePresenter.kt
index 8ef0772b8..c00bdc3e4 100644
--- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetablePresenter.kt
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetablePresenter.kt
@@ -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
@@ -81,7 +85,7 @@ class TimetablePresenter @Inject constructor(
} else currentDate?.previousSchoolDay
reloadView(date ?: return)
- loadData()
+ loadData(isDayChanged = true)
}
fun onNextDay() {
@@ -90,7 +94,7 @@ class TimetablePresenter @Inject constructor(
} else currentDate?.nextSchoolDay
reloadView(date ?: return)
- loadData()
+ loadData(isDayChanged = true)
}
fun onPickDate() {
@@ -104,7 +108,7 @@ class TimetablePresenter @Inject constructor(
fun onSwipeRefresh() {
Timber.i("Force refreshing the timetable")
- loadData(true)
+ loadData(forceRefresh = true)
}
fun onRetry() {
@@ -112,7 +116,7 @@ class TimetablePresenter @Inject constructor(
showErrorView(false)
showProgress(true)
}
- loadData(true)
+ loadData(forceRefresh = true)
}
fun onDetailsClick() {
@@ -145,7 +149,7 @@ 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)
@@ -169,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()
}
@@ -216,67 +220,97 @@ class TimetablePresenter @Inject constructor(
}
}
- private fun updateData(lessons: List) {
+ 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): List {
- val filteredItems = items
- .filter {
- if (prefRepository.showWholeClassPlan == TimetableMode.ONLY_CURRENT_GROUP) {
- it.isStudentPlan
- } else true
- }
- .sortedWith(compareBy({ item -> item.start }, { 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 {
+ 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- { 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,
- isLessonNumberVisible = !isEduOne
- )
- add(normalLesson)
- } else {
- val smallLesson = TimetableItem.Small(
- lesson = it,
- onClick = ::onTimetableItemSelected,
- isLessonNumberVisible = !isEduOne
- )
- 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.getTimeLeftForLesson(lesson: Timetable, index: Int): TimeLeft {
+ private fun List
- .getTimeLeftForLesson(lesson: Timetable, index: Int): TimeLeft {
val isShowTimeUntil = lesson.isShowTimeUntil(getPreviousLesson(index))
return TimeLeft(
until = lesson.until.plusMinutes(1).takeIf { isShowTimeUntil },
@@ -285,11 +319,20 @@ class TimetablePresenter @Inject constructor(
)
}
- private fun List.getPreviousLesson(position: Int): Instant? {
- return filter { it.isStudentPlan }
- .getOrNull(position - 1 - filterIndexed { i, item -> i < position && !item.isStudentPlan }.size)
+ private fun List
- .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()
+ .filter { it.isStudentPlan }
+ .getOrNull(lessonIndex)
?.let {
- if (!it.canceled && it.isStudentPlan) it.end
+ if (!it.lesson.canceled && it.isStudentPlan) it.lesson.end
else null
}
}
@@ -342,3 +385,7 @@ class TimetablePresenter @Inject constructor(
super.onDetachView()
}
}
+
+private class EmptyComparator : Comparator {
+ override fun compare(o1: T, o2: T) = 0
+}
diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableView.kt
index 40190d51f..f4d5b7621 100644
--- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableView.kt
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableView.kt
@@ -12,7 +12,7 @@ interface TimetableView : BaseView {
fun initView()
- fun updateData(data: List)
+ fun updateData(data: List, isDayChanged: Boolean)
fun updateNavigationDay(date: String)
diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/AdditionalLessonsFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/AdditionalLessonsFragment.kt
index faa833c20..bf6be56f6 100644
--- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/AdditionalLessonsFragment.kt
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/AdditionalLessonsFragment.kt
@@ -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) {
diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/AdditionalLessonsPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/AdditionalLessonsPresenter.kt
index d0a01b38c..16ec9746f 100644
--- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/AdditionalLessonsPresenter.kt
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/AdditionalLessonsPresenter.kt
@@ -1,14 +1,27 @@
package io.github.wulkanowy.ui.modules.timetable.additional
import android.annotation.SuppressLint
-import io.github.wulkanowy.data.*
import io.github.wulkanowy.data.db.entities.TimetableAdditional
+import io.github.wulkanowy.data.flatResourceFlow
+import io.github.wulkanowy.data.logResourceStatus
+import io.github.wulkanowy.data.onResourceData
+import io.github.wulkanowy.data.onResourceError
+import io.github.wulkanowy.data.onResourceNotLoading
+import io.github.wulkanowy.data.onResourceSuccess
import io.github.wulkanowy.data.repositories.SemesterRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.repositories.TimetableRepository
+import io.github.wulkanowy.domain.timetable.IsStudentHasLessonsOnWeekendUseCase
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
-import io.github.wulkanowy.utils.*
+import io.github.wulkanowy.utils.AnalyticsHelper
+import io.github.wulkanowy.utils.capitalise
+import io.github.wulkanowy.utils.getLastSchoolDayIfHoliday
+import io.github.wulkanowy.utils.isHolidays
+import io.github.wulkanowy.utils.nextOrSameSchoolDay
+import io.github.wulkanowy.utils.nextSchoolDay
+import io.github.wulkanowy.utils.previousSchoolDay
+import io.github.wulkanowy.utils.toFormattedString
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.onEach
@@ -22,11 +35,14 @@ class AdditionalLessonsPresenter @Inject constructor(
errorHandler: ErrorHandler,
private val semesterRepository: SemesterRepository,
private val timetableRepository: TimetableRepository,
+ private val isStudentHasLessonsOnWeekendUseCase: IsStudentHasLessonsOnWeekendUseCase,
private val analytics: AnalyticsHelper
) : BasePresenter(errorHandler, studentRepository) {
private var baseDate: LocalDate = LocalDate.now().nextOrSameSchoolDay
+ private var isWeekendHasLessons: Boolean = false
+
lateinit var currentDate: LocalDate
private set
@@ -43,12 +59,18 @@ class AdditionalLessonsPresenter @Inject constructor(
}
fun onPreviousDay() {
- loadData(currentDate.previousSchoolDay)
+ val date = if (isWeekendHasLessons) {
+ currentDate.minusDays(1)
+ } else currentDate.previousSchoolDay
+ loadData(date)
reloadView()
}
fun onNextDay() {
- loadData(currentDate.nextSchoolDay)
+ val date = if (isWeekendHasLessons) {
+ currentDate.plusDays(1)
+ } else currentDate.nextSchoolDay
+ loadData(date)
reloadView()
}
@@ -57,7 +79,7 @@ class AdditionalLessonsPresenter @Inject constructor(
}
fun onAdditionalLessonAddButtonClicked() {
- view?.showAddAdditionalLessonDialog()
+ view?.showAddAdditionalLessonDialog(currentDate)
}
fun onDateSet(year: Int, month: Int, day: Int) {
@@ -131,6 +153,8 @@ class AdditionalLessonsPresenter @Inject constructor(
flatResourceFlow {
val student = studentRepository.getCurrentStudent()
val semester = semesterRepository.getCurrentSemester(student)
+
+ isWeekendHasLessons = isStudentHasLessonsOnWeekendUseCase(semester, currentDate)
timetableRepository.getTimetable(
student = student,
semester = semester,
diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/AdditionalLessonsView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/AdditionalLessonsView.kt
index 76d37b754..291c12172 100644
--- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/AdditionalLessonsView.kt
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/AdditionalLessonsView.kt
@@ -36,7 +36,7 @@ interface AdditionalLessonsView : BaseView {
fun showDatePickerDialog(selectedDate: LocalDate)
- fun showAddAdditionalLessonDialog()
+ fun showAddAdditionalLessonDialog(currentDate: LocalDate)
fun showSuccessMessage()
diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/add/AdditionalLessonAddDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/add/AdditionalLessonAddDialog.kt
index 134719979..9470c910f 100644
--- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/add/AdditionalLessonAddDialog.kt
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/add/AdditionalLessonAddDialog.kt
@@ -3,6 +3,7 @@ package io.github.wulkanowy.ui.modules.timetable.additional.add
import android.app.Dialog
import android.os.Bundle
import android.view.View
+import androidx.core.os.bundleOf
import androidx.core.widget.doOnTextChanged
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.timepicker.MaterialTimePicker
@@ -26,10 +27,12 @@ class AdditionalLessonAddDialog : BaseDialogFragment
lateinit var presenter: AdditionalLessonAddPresenter
companion object {
- fun newInstance() = AdditionalLessonAddDialog()
+ const val ARGUMENT_KEY = "additional_lesson_default_date"
+ fun newInstance(defaultDate: LocalDate) = AdditionalLessonAddDialog().apply {
+ arguments = bundleOf(ARGUMENT_KEY to defaultDate.toEpochDay())
+ }
}
-
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return MaterialAlertDialogBuilder(requireContext(), theme)
.setView(
@@ -40,10 +43,13 @@ class AdditionalLessonAddDialog : BaseDialogFragment
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
+ arguments?.getLong(ARGUMENT_KEY)?.let(LocalDate::ofEpochDay)?.let {
+ presenter.onDateSelected(it)
+ }
presenter.onAttachView(this)
}
- override fun initView() {
+ override fun initView(selectedDate: LocalDate) {
with(binding) {
additionalLessonDialogStartEdit.doOnTextChanged { _, _, _, _ ->
additionalLessonDialogStart.isErrorEnabled = false
@@ -53,6 +59,7 @@ class AdditionalLessonAddDialog : BaseDialogFragment
additionalLessonDialogEnd.isErrorEnabled = false
additionalLessonDialogEnd.error = null
}
+ additionalLessonDialogDateEdit.setText(selectedDate.toFormattedString())
additionalLessonDialogDateEdit.doOnTextChanged { _, _, _, _ ->
additionalLessonDialogDate.isErrorEnabled = false
additionalLessonDialogDate.error = null
@@ -61,7 +68,6 @@ class AdditionalLessonAddDialog : BaseDialogFragment
additionalLessonDialogContent.isErrorEnabled = false
additionalLessonDialogContent.error = null
}
-
additionalLessonDialogAdd.setOnClickListener {
presenter.onAddAdditionalClicked(
start = additionalLessonDialogStartEdit.text?.toString(),
@@ -155,7 +161,9 @@ class AdditionalLessonAddDialog : BaseDialogFragment
.build()
timePicker.addOnPositiveButtonClickListener {
- onTimeSelected(LocalTime.of(timePicker.hour, timePicker.minute))
+ if (isAdded) {
+ onTimeSelected(LocalTime.of(timePicker.hour, timePicker.minute))
+ }
}
if (!parentFragmentManager.isStateSaved) {
diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/add/AdditionalLessonAddPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/add/AdditionalLessonAddPresenter.kt
index c207165d3..db59a2ab5 100644
--- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/add/AdditionalLessonAddPresenter.kt
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/add/AdditionalLessonAddPresenter.kt
@@ -10,9 +10,12 @@ import io.github.wulkanowy.utils.lastSchoolDayInSchoolYear
import io.github.wulkanowy.utils.toLocalDate
import kotlinx.coroutines.launch
import timber.log.Timber
-import java.time.*
+import java.time.LocalDate
+import java.time.LocalTime
+import java.time.ZoneId
+import java.time.ZonedDateTime
import java.time.temporal.ChronoUnit
-import java.util.*
+import java.util.UUID
import javax.inject.Inject
class AdditionalLessonAddPresenter @Inject constructor(
@@ -30,7 +33,7 @@ class AdditionalLessonAddPresenter @Inject constructor(
override fun onAttachView(view: AdditionalLessonAddView) {
super.onAttachView(view)
- view.initView()
+ view.initView(selectedDate)
Timber.i("AdditionalLesson details view was initialized")
}
diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/add/AdditionalLessonAddView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/add/AdditionalLessonAddView.kt
index 0df53815b..8d9678e7b 100644
--- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/add/AdditionalLessonAddView.kt
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/add/AdditionalLessonAddView.kt
@@ -6,7 +6,7 @@ import java.time.LocalTime
interface AdditionalLessonAddView : BaseView {
- fun initView()
+ fun initView(selectedDate: LocalDate)
fun closeDialog()
diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetFactory.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetFactory.kt
index 218c25834..e60d54880 100644
--- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetFactory.kt
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetFactory.kt
@@ -101,7 +101,14 @@ class TimetableWidgetFactory(
private suspend fun getLessons(
student: Student, semester: Semester, date: LocalDate
): List {
- val timetable = timetableRepository.getTimetable(student, semester, date, date, false)
+ val timetable = timetableRepository.getTimetable(
+ student = student,
+ semester = semester,
+ start = date,
+ end = date,
+ forceRefresh = false,
+ isFromAppWidget = true
+ )
val lessons = timetable.toFirstResult().dataOrThrow.lessons
return lessons.sortedBy { it.start }
}
diff --git a/app/src/main/java/io/github/wulkanowy/utils/AppWidgetUpdater.kt b/app/src/main/java/io/github/wulkanowy/utils/AppWidgetUpdater.kt
new file mode 100644
index 000000000..1b54f40c1
--- /dev/null
+++ b/app/src/main/java/io/github/wulkanowy/utils/AppWidgetUpdater.kt
@@ -0,0 +1,34 @@
+package io.github.wulkanowy.utils
+
+import android.appwidget.AppWidgetManager
+import android.content.BroadcastReceiver
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import dagger.hilt.android.qualifiers.ApplicationContext
+import timber.log.Timber
+import javax.inject.Inject
+import kotlin.reflect.KClass
+
+class AppWidgetUpdater @Inject constructor(
+ @ApplicationContext private val context: Context,
+ private val appWidgetManager: AppWidgetManager
+) {
+
+ fun updateAllAppWidgetsByProvider(providerClass: KClass) {
+ try {
+ val ids = appWidgetManager.getAppWidgetIds(ComponentName(context, providerClass.java))
+ if (ids.isEmpty()) return
+
+ val intent = Intent(context, providerClass.java)
+ .apply {
+ action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
+ putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids)
+ }
+
+ context.sendBroadcast(intent)
+ } catch (e: Exception) {
+ Timber.e(e, "Failed to update all widgets for provider $providerClass")
+ }
+ }
+}
diff --git a/app/src/main/java/io/github/wulkanowy/utils/AttendanceExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/AttendanceExtension.kt
index 397c95953..3cac0b48e 100644
--- a/app/src/main/java/io/github/wulkanowy/utils/AttendanceExtension.kt
+++ b/app/src/main/java/io/github/wulkanowy/utils/AttendanceExtension.kt
@@ -10,19 +10,19 @@ import io.github.wulkanowy.sdk.scrapper.attendance.AttendanceCategory
* (https://www.vulcan.edu.pl/vulcang_files/user/AABW/AABW-PDF/uonetplus/uonetplus_Frekwencja-liczby-obecnych-nieobecnych.pdf)
*/
-private inline val AttendanceSummary.allPresences: Double
- get() = presence.toDouble() + absenceForSchoolReasons + lateness + latenessExcused
+inline val AttendanceSummary.allPresences: Int
+ get() = presence + absenceForSchoolReasons + lateness + latenessExcused
-private inline val AttendanceSummary.allAbsences: Double
- get() = absence.toDouble() + absenceExcused
+inline val AttendanceSummary.allAbsences: Int
+ get() = absence + absenceExcused
inline val Attendance.isExcusableOrNotExcused: Boolean
get() = (excusable || ((absence || lateness) && !excused)) && excuseStatus == null
-fun AttendanceSummary.calculatePercentage() = calculatePercentage(allPresences, allAbsences)
+fun AttendanceSummary.calculatePercentage() = calculatePercentage(allPresences.toDouble(), allAbsences.toDouble())
fun List.calculatePercentage(): Double {
- return calculatePercentage(sumOf { it.allPresences }, sumOf { it.allAbsences })
+ return calculatePercentage(sumOf { it.allPresences.toDouble() }, sumOf { it.allAbsences.toDouble() })
}
private fun calculatePercentage(presence: Double, absence: Double): Double {
diff --git a/app/src/main/java/io/github/wulkanowy/utils/RefreshUtils.kt b/app/src/main/java/io/github/wulkanowy/utils/RefreshUtils.kt
index 721297513..4cce4be43 100644
--- a/app/src/main/java/io/github/wulkanowy/utils/RefreshUtils.kt
+++ b/app/src/main/java/io/github/wulkanowy/utils/RefreshUtils.kt
@@ -23,13 +23,17 @@ fun getRefreshKey(name: String, semester: Semester): String {
}
fun getRefreshKey(name: String, student: Student): String {
- return "${name}_${student.userLoginId}"
+ return "${name}_${student.studentId}"
}
fun getRefreshKey(name: String, mailbox: Mailbox?, folder: MessageFolder): String {
return "${name}_${mailbox?.globalKey ?: "all"}_${folder.id}"
}
+fun getRefreshKey(name: String): String {
+ return name
+}
+
class AutoRefreshHelper @Inject constructor(
@ApplicationContext private val context: Context,
private val sharedPref: SharedPrefProvider
diff --git a/app/src/main/java/io/github/wulkanowy/utils/SyncListAdapter.kt b/app/src/main/java/io/github/wulkanowy/utils/SyncListAdapter.kt
new file mode 100644
index 000000000..e9135f498
--- /dev/null
+++ b/app/src/main/java/io/github/wulkanowy/utils/SyncListAdapter.kt
@@ -0,0 +1,66 @@
+package io.github.wulkanowy.utils
+
+import android.annotation.SuppressLint
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.RecyclerView
+
+/**
+ * Custom alternative to androidx.recyclerview.widget.ListAdapter. ListAdapter is asynchronous which
+ * caused data race problems in views when a Resource.Error arrived shortly after
+ * Resource.Intermediate/Success - occasionally in that case the user could see both the Resource's
+ * data and an error message one on top of the other. This is synchronized by design to avoid that
+ * problem, however it retains the quality of life improvements of the original.
+ */
+abstract class SyncListAdapter private constructor(
+ private val updateStrategy: SyncListAdapter.(List) -> Unit
+) : RecyclerView.Adapter() {
+
+ constructor(differ: DiffUtil.ItemCallback) : this({ newItems ->
+ val diffResult = DiffUtil.calculateDiff(toCallback(differ, items, newItems))
+ items = newItems
+ diffResult.dispatchUpdatesTo(this)
+ })
+
+ var items = emptyList()
+ private set
+
+ final override fun getItemCount() = items.size
+
+ fun getItem(position: Int): T {
+ return items[position]
+ }
+
+ /**
+ * Updates all items, same as submitList, however also disables animations temporarily.
+ * This prevents a flashing effect on some views. Should be used in favor of submitList when
+ * all data is changed (e.g. the selected day changes in timetable causing all lessons to change).
+ */
+ @SuppressLint("NotifyDataSetChanged")
+ fun recreate(data: List) {
+ items = data
+ notifyDataSetChanged()
+ }
+
+ fun submitList(data: List) {
+ updateStrategy(data.toList())
+ }
+
+ private fun toCallback(
+ itemCallback: DiffUtil.ItemCallback,
+ old: List,
+ new: List,
+ ) = object : DiffUtil.Callback() {
+ override fun getOldListSize() = old.size
+
+ override fun getNewListSize() = new.size
+
+ override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =
+ itemCallback.areItemsTheSame(old[oldItemPosition], new[newItemPosition])
+
+ override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) =
+ itemCallback.areContentsTheSame(old[oldItemPosition], new[newItemPosition])
+
+ override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int) =
+ itemCallback.getChangePayload(old[oldItemPosition], new[newItemPosition])
+ }
+}
diff --git a/app/src/main/java/io/github/wulkanowy/utils/TimeExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/TimeExtension.kt
index e7a50d0c3..77689fcb7 100644
--- a/app/src/main/java/io/github/wulkanowy/utils/TimeExtension.kt
+++ b/app/src/main/java/io/github/wulkanowy/utils/TimeExtension.kt
@@ -1,14 +1,32 @@
package io.github.wulkanowy.utils
import java.text.SimpleDateFormat
-import java.time.*
-import java.time.DayOfWeek.*
+import java.time.DayOfWeek.FRIDAY
+import java.time.DayOfWeek.MONDAY
+import java.time.DayOfWeek.SATURDAY
+import java.time.DayOfWeek.SUNDAY
+import java.time.Instant
+import java.time.LocalDate
+import java.time.LocalDateTime
+import java.time.Month
+import java.time.ZoneId
+import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
-import java.time.temporal.TemporalAdjusters.*
-import java.util.*
+import java.time.temporal.TemporalAdjusters.firstInMonth
+import java.time.temporal.TemporalAdjusters.next
+import java.time.temporal.TemporalAdjusters.previous
+import java.util.Locale
private const val DEFAULT_DATE_PATTERN = "dd.MM.yyyy"
+fun getDefaultLocaleWithFallback(): Locale {
+ val locale = Locale.getDefault()
+ if (locale.language == "csb") {
+ return Locale.forLanguageTag("pl")
+ }
+ return locale
+}
+
fun LocalDate.toTimestamp(): Long = atStartOfDay()
.toInstant(ZoneOffset.UTC)
.toEpochMilli()
@@ -23,7 +41,7 @@ fun String.toLocalDate(format: String = DEFAULT_DATE_PATTERN): LocalDate =
LocalDate.parse(this, DateTimeFormatter.ofPattern(format))
fun LocalDate.toFormattedString(pattern: String = DEFAULT_DATE_PATTERN): String =
- format(DateTimeFormatter.ofPattern(pattern))
+ format(DateTimeFormatter.ofPattern(pattern, getDefaultLocaleWithFallback()))
fun Instant.toFormattedString(
pattern: String = DEFAULT_DATE_PATTERN,
@@ -31,7 +49,7 @@ fun Instant.toFormattedString(
): String = atZone(tz).format(DateTimeFormatter.ofPattern(pattern))
fun Month.getFormattedName(): String {
- val formatter = SimpleDateFormat("LLLL", Locale.getDefault())
+ val formatter = SimpleDateFormat("LLLL", getDefaultLocaleWithFallback())
val date = LocalDateTime.now().withMonth(value)
return formatter.format(date.toInstant(ZoneOffset.UTC).toEpochMilli()).capitalise()
@@ -76,7 +94,7 @@ inline val LocalDate.previousOrSameSchoolDay: LocalDate
}
inline val LocalDate.weekDayName: String
- get() = format(DateTimeFormatter.ofPattern("EEEE", Locale.getDefault()))
+ get() = format(DateTimeFormatter.ofPattern("EEEE", getDefaultLocaleWithFallback()))
inline val LocalDate.monday: LocalDate get() = with(MONDAY)
diff --git a/app/src/main/play/listings/cs-CZ/full-description.txt b/app/src/main/play/listings/cs-CZ/full-description.txt
index 1420f5d67..3d142ebff 100644
--- a/app/src/main/play/listings/cs-CZ/full-description.txt
+++ b/app/src/main/play/listings/cs-CZ/full-description.txt
@@ -6,7 +6,7 @@ Zvýrazněné vlastnosti a funkce:
- šťastné číslo,
- náhled na další a dokončené lekce,
- tmavý motiv,
-- žádné reklamy,
+- volitelné reklamy,
- offline režim,
- upozornění.
diff --git a/app/src/main/play/listings/pl-PL/full-description.txt b/app/src/main/play/listings/pl-PL/full-description.txt
index 7da51da2d..b0193b5d1 100644
--- a/app/src/main/play/listings/pl-PL/full-description.txt
+++ b/app/src/main/play/listings/pl-PL/full-description.txt
@@ -6,7 +6,7 @@ Wyróżnione cechy i funkcje:
- szczęśliwy numerek,
- podgląd lekcji dodatkowych i zrealizowanych,
- ciemny motyw.
-- brak reklam,
+- opcjonalne reklam,
- tryb offline,
- powiadomienia.
diff --git a/app/src/main/play/listings/pl-PL/graphics/phone-screenshots/1-start.jpg b/app/src/main/play/listings/pl-PL/graphics/phone-screenshots/1-start.jpg
index 0ed20c04d..19b96d9a6 100644
Binary files a/app/src/main/play/listings/pl-PL/graphics/phone-screenshots/1-start.jpg and b/app/src/main/play/listings/pl-PL/graphics/phone-screenshots/1-start.jpg differ
diff --git a/app/src/main/play/listings/pl-PL/graphics/phone-screenshots/2.jpg b/app/src/main/play/listings/pl-PL/graphics/phone-screenshots/2.jpg
index f70e2c43b..fe81aadce 100644
Binary files a/app/src/main/play/listings/pl-PL/graphics/phone-screenshots/2.jpg and b/app/src/main/play/listings/pl-PL/graphics/phone-screenshots/2.jpg differ
diff --git a/app/src/main/play/listings/pl-PL/graphics/phone-screenshots/3-timetable-dialog.jpg b/app/src/main/play/listings/pl-PL/graphics/phone-screenshots/3-timetable-dialog.jpg
index 968fccdbe..0e62c2e10 100644
Binary files a/app/src/main/play/listings/pl-PL/graphics/phone-screenshots/3-timetable-dialog.jpg and b/app/src/main/play/listings/pl-PL/graphics/phone-screenshots/3-timetable-dialog.jpg differ
diff --git a/app/src/main/play/listings/pl-PL/graphics/phone-screenshots/4-exams.jpg b/app/src/main/play/listings/pl-PL/graphics/phone-screenshots/4-exams.jpg
index 3f49e774a..980993af1 100644
Binary files a/app/src/main/play/listings/pl-PL/graphics/phone-screenshots/4-exams.jpg and b/app/src/main/play/listings/pl-PL/graphics/phone-screenshots/4-exams.jpg differ
diff --git a/app/src/main/play/listings/pl-PL/graphics/phone-screenshots/5-timetable-widget.jpg b/app/src/main/play/listings/pl-PL/graphics/phone-screenshots/5-timetable-widget.jpg
index f68daaf1a..6ee7eeb95 100644
Binary files a/app/src/main/play/listings/pl-PL/graphics/phone-screenshots/5-timetable-widget.jpg and b/app/src/main/play/listings/pl-PL/graphics/phone-screenshots/5-timetable-widget.jpg differ
diff --git a/app/src/main/play/listings/pl-PL/graphics/phone-screenshots/6-class-grades.jpg b/app/src/main/play/listings/pl-PL/graphics/phone-screenshots/6-class-grades.jpg
index ca5446a24..ec86cdb0d 100644
Binary files a/app/src/main/play/listings/pl-PL/graphics/phone-screenshots/6-class-grades.jpg and b/app/src/main/play/listings/pl-PL/graphics/phone-screenshots/6-class-grades.jpg differ
diff --git a/app/src/main/play/listings/pl-PL/graphics/phone-screenshots/7-account-switcher.jpg b/app/src/main/play/listings/pl-PL/graphics/phone-screenshots/7-account-switcher.jpg
index ca747aff0..66c0db40d 100644
Binary files a/app/src/main/play/listings/pl-PL/graphics/phone-screenshots/7-account-switcher.jpg and b/app/src/main/play/listings/pl-PL/graphics/phone-screenshots/7-account-switcher.jpg differ
diff --git a/app/src/main/play/listings/pl-PL/graphics/phone-screenshots/8-themes.jpg b/app/src/main/play/listings/pl-PL/graphics/phone-screenshots/8-themes.jpg
index ce3af9bbf..94788cd7f 100644
Binary files a/app/src/main/play/listings/pl-PL/graphics/phone-screenshots/8-themes.jpg and b/app/src/main/play/listings/pl-PL/graphics/phone-screenshots/8-themes.jpg differ
diff --git a/app/src/main/play/listings/sk/full-description.txt b/app/src/main/play/listings/sk/full-description.txt
index 2a4787d2d..236825984 100644
--- a/app/src/main/play/listings/sk/full-description.txt
+++ b/app/src/main/play/listings/sk/full-description.txt
@@ -6,7 +6,7 @@ Zvýraznené vlastnosti a funkcie:
- šťastné číslo,
- náhľad na ďalšie a dokončené lekcie,
- tmavý motív,
-- žiadne reklamy,
+- voliteľné reklamy,
- offline režim,
- upozornenia.
diff --git a/app/src/main/play/release-notes/pl-PL/default.txt b/app/src/main/play/release-notes/pl-PL/default.txt
index 9d72cb076..329da27f1 100644
--- a/app/src/main/play/release-notes/pl-PL/default.txt
+++ b/app/src/main/play/release-notes/pl-PL/default.txt
@@ -1,6 +1,5 @@
-Wersja 2.5.5
+Wersja 2.6.15
-— naprawiliśmy migrację informacji o tym, czy szkoła ucznia używa eduOne
-— naprawiliśmy w końcu (teraz naprawdę mamy taką nadzieję) ten komunikat o braku uprawnień
+— naprawiliśmy moduł wiadomości
Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases
diff --git a/app/src/main/res/drawable/ic_menu_attendance_calculator.xml b/app/src/main/res/drawable/ic_menu_attendance_calculator.xml
new file mode 100644
index 000000000..8a7d209a7
--- /dev/null
+++ b/app/src/main/res/drawable/ic_menu_attendance_calculator.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/layout/dialog_auth.xml b/app/src/main/res/layout/dialog_auth.xml
index e2e2aa304..a0b9d6ea7 100644
--- a/app/src/main/res/layout/dialog_auth.xml
+++ b/app/src/main/res/layout/dialog_auth.xml
@@ -32,12 +32,11 @@
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
android:layout_marginTop="8dp"
- android:textSize="16sp"
+ android:textSize="14sp"
app:layout_constraintTop_toBottomOf="@id/auth_title"
- app:lineHeight="24sp"
+ app:lineHeight="18sp"
tools:text="@string/auth_description" />
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_login_student_select.xml b/app/src/main/res/layout/fragment_login_student_select.xml
index 04c808857..6b603c9f0 100644
--- a/app/src/main/res/layout/fragment_login_student_select.xml
+++ b/app/src/main/res/layout/fragment_login_student_select.xml
@@ -11,6 +11,18 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/header_grade_details.xml b/app/src/main/res/layout/header_grade_details.xml
index e43e8993f..1765d9d1e 100644
--- a/app/src/main/res/layout/header_grade_details.xml
+++ b/app/src/main/res/layout/header_grade_details.xml
@@ -45,13 +45,30 @@
android:textColor="?android:textColorSecondary"
android:textSize="12sp"
app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintEnd_toStartOf="@id/gradeHeaderPointsSum"
+ app:layout_constraintEnd_toStartOf="@id/gradeHeaderAverageAllYear"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="@id/gradeHeaderSubject"
app:layout_constraintTop_toBottomOf="@+id/gradeHeaderSubject"
tools:text="Average: 6,00" />
+
+
diff --git a/app/src/main/res/layout/item_attendance_calculator_header.xml b/app/src/main/res/layout/item_attendance_calculator_header.xml
new file mode 100644
index 000000000..debc79979
--- /dev/null
+++ b/app/src/main/res/layout/item_attendance_calculator_header.xml
@@ -0,0 +1,111 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/item_dashboard_admin_message.xml b/app/src/main/res/layout/item_dashboard_admin_message.xml
index 407e12921..0d519b7ea 100644
--- a/app/src/main/res/layout/item_dashboard_admin_message.xml
+++ b/app/src/main/res/layout/item_dashboard_admin_message.xml
@@ -1,87 +1,105 @@
-
+ android:orientation="vertical">
-
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="12dp"
+ android:layout_marginVertical="6dp">
-
-
+
-
+
-
+
-
-
-
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/item_dashboard_panic_button.xml b/app/src/main/res/layout/item_dashboard_panic_button.xml
new file mode 100644
index 000000000..5f5936cf1
--- /dev/null
+++ b/app/src/main/res/layout/item_dashboard_panic_button.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/item_grade_summary.xml b/app/src/main/res/layout/item_grade_summary.xml
index 2c8c4ea37..f425bad83 100644
--- a/app/src/main/res/layout/item_grade_summary.xml
+++ b/app/src/main/res/layout/item_grade_summary.xml
@@ -20,20 +20,80 @@
android:id="@+id/gradeSummaryItemTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
- android:layout_marginEnd="40dp"
android:layout_weight="1"
android:textSize="17sp"
tools:text="@tools:sample/lorem" />
+
+
+
+
+
+
+ tools:text="2,50" />
+
+
+
+
+
+
+
+
+
+
+
@@ -131,9 +191,9 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/pref_target_attendance.xml b/app/src/main/res/layout/pref_target_attendance.xml
new file mode 100644
index 000000000..558b0d36f
--- /dev/null
+++ b/app/src/main/res/layout/pref_target_attendance.xml
@@ -0,0 +1,140 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/scrollable_header_grade_summary.xml b/app/src/main/res/layout/scrollable_header_grade_summary.xml
index 049219a98..e1b898873 100644
--- a/app/src/main/res/layout/scrollable_header_grade_summary.xml
+++ b/app/src/main/res/layout/scrollable_header_grade_summary.xml
@@ -10,10 +10,11 @@
tools:context=".ui.modules.grade.summary.GradeSummaryAdapter">
+ android:textSize="16sp"
+ android:textStyle="bold" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ android:textSize="16sp"
+ android:textStyle="bold" />
diff --git a/app/src/main/res/menu/action_menu_attendance.xml b/app/src/main/res/menu/action_menu_attendance.xml
index bb20c8ec2..5c59d2391 100644
--- a/app/src/main/res/menu/action_menu_attendance.xml
+++ b/app/src/main/res/menu/action_menu_attendance.xml
@@ -1,6 +1,13 @@