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/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 000000000..cdce0759b
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,4 @@
+# These are supported funding model platforms
+
+github: wulkanowy
+custom: https://www.paypal.com/paypalme/wulkanowy
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 980085e38..ad83ced8d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -67,6 +67,10 @@ captures/
.idea/discord.xml
.idea/migrations.xml
.idea/androidTestResultsUserPreferences.xml
+.idea/copilot
+.idea/deploymentTargetDropDown.xml
+.idea/deploymentTargetSelector.xml
+.idea/kotlinc.xml
# Keystore files
*.jks
@@ -113,12 +117,13 @@ Thumbs.db
*.ear
### AndroidStudio Patch ###
-
!/gradle/wrapper/gradle-wrapper.jar
.idea/jarRepositories.xml
+### Services config files
+agconnect-services.json
+agconnect-credentials.json
+google-services.json
+!app/google-services.json
+
-app/src/release/agconnect-services.json
-app/src/release/agconnect-credentials.json
-.idea/deploymentTargetDropDown.xml
-.idea/kotlinc.xml
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 1408fedf1..d1c67ab0a 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -27,15 +27,12 @@ android {
testApplicationId "io.github.tests.wulkanowy"
minSdkVersion 21
targetSdkVersion 34
- versionCode 147
- versionName "2.4.1"
+ versionCode 150
+ versionName "2.5.1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resValue "string", "app_name", "Wulkanowy"
- manifestPlaceholders = [
- firebase_enabled: project.hasProperty("enableFirebase"),
- admob_project_id: ""
- ]
+ manifestPlaceholders = [admob_project_id: ""]
buildConfigField "String", "SINGLE_SUPPORT_AD_ID", "null"
buildConfigField "String", "DASHBOARD_TILE_AD_ID", "null"
@@ -76,7 +73,6 @@ android {
resValue "string", "app_name", "Wulkanowy DEV"
applicationIdSuffix ".dev"
versionNameSuffix "-dev"
- ext.enableCrashlytics = project.hasProperty("enableFirebase")
buildConfigField "String", "MESSAGES_BASE_URL", "\"https://messages.wulkanowy.net.pl\""
buildConfigField "String", "SCHOOLS_BASE_URL", '"https://schools.wulkanowy.net.pl"'
}
@@ -187,15 +183,15 @@ huaweiPublish {
ext {
work_manager = "2.9.0"
- android_hilt = "1.1.0"
+ android_hilt = "1.2.0"
room = "2.6.1"
chucker = "4.0.0"
- mockk = "1.13.9"
+ mockk = "1.13.10"
coroutines = "1.8.0"
}
dependencies {
- implementation 'io.github.wulkanowy:sdk:2.4.1'
+ implementation 'io.github.wulkanowy:sdk:2.5.1'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
@@ -246,23 +242,23 @@ dependencies {
implementation 'com.github.Faierbel:slf4j-timber:2.0'
implementation 'com.github.bastienpaulfr:Treessence:1.1.2'
implementation "com.mikepenz:aboutlibraries-core:$about_libraries"
- implementation 'io.coil-kt:coil:2.5.0'
+ implementation 'io.coil-kt:coil:2.6.0'
implementation "io.github.wulkanowy:AppKillerManager:3.0.1"
implementation 'me.xdrop:fuzzywuzzy:1.4.0'
implementation 'com.fredporciuncula:flow-preferences:1.9.1'
implementation 'org.apache.commons:commons-text:1.11.0'
- playImplementation platform('com.google.firebase:firebase-bom:32.7.2')
+ playImplementation platform('com.google.firebase:firebase-bom:32.7.4')
playImplementation 'com.google.firebase:firebase-analytics'
playImplementation 'com.google.firebase:firebase-messaging'
playImplementation 'com.google.firebase:firebase-crashlytics:'
playImplementation 'com.google.firebase:firebase-config'
- playImplementation 'com.google.android.gms:play-services-ads:22.6.0'
+ playImplementation 'com.google.android.gms:play-services-ads:23.0.0'
playImplementation "com.google.android.play:integrity:1.3.0"
playImplementation 'com.google.android.play:app-update-ktx:2.1.0'
playImplementation 'com.google.android.play:review-ktx:2.0.1'
- playImplementation "com.google.android.ump:user-messaging-platform:2.2.0"
+ playImplementation "com.google.android.ump:user-messaging-platform:2.1.0"
hmsImplementation 'com.huawei.hms:hianalytics:6.12.0.301'
hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.9.1.303'
diff --git a/app/src/debug/google-services.json b/app/google-services.json
similarity index 100%
rename from app/src/debug/google-services.json
rename to app/google-services.json
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/60.json b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/60.json
new file mode 100644
index 000000000..20eacad1c
--- /dev/null
+++ b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/60.json
@@ -0,0 +1,2527 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 60,
+ "identityHash": "3672d3f4d5e6b874e5a22d2bb458dc65",
+ "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, `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": "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, `average` REAL NOT NULL, `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": "average",
+ "columnName": "average",
+ "affinity": "REAL",
+ "notNull": true
+ },
+ {
+ "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": "userLoginId",
+ "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, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "userLoginId",
+ "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": "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, '3672d3f4d5e6b874e5a22d2bb458dc65')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/schemas/io.github.wulkanowy.data.db.AppDatabase/61.json b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/61.json
new file mode 100644
index 000000000..e36dcc8a6
--- /dev/null
+++ b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/61.json
@@ -0,0 +1,2533 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 61,
+ "identityHash": "41fbd2ff00aba10b2ef0a079e6037c87",
+ "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, `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": "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, `average` REAL NOT NULL, `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": "average",
+ "columnName": "average",
+ "affinity": "REAL",
+ "notNull": true
+ },
+ {
+ "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": "userLoginId",
+ "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": "userLoginId",
+ "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, '41fbd2ff00aba10b2ef0a079e6037c87')"
+ ]
+ }
+}
\ 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..4e617c931 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -155,33 +155,9 @@
android:resource="@xml/provider_paths" />
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/assets/contributors.json b/app/src/main/assets/contributors.json
index a7629c22f..97ac9356f 100644
--- a/app/src/main/assets/contributors.json
+++ b/app/src/main/assets/contributors.json
@@ -54,5 +54,9 @@
{
"displayName": "Antoni Paduch",
"githubUsername": "janAte1"
+ },
+ {
+ "displayName": "Kamil Wąsik",
+ "githubUsername": "JestemKamil"
}
]
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 7c9cf9a3c..a492c08db 100644
--- a/app/src/main/java/io/github/wulkanowy/data/DataModule.kt
+++ b/app/src/main/java/io/github/wulkanowy/data/DataModule.kt
@@ -18,17 +18,13 @@ import io.github.wulkanowy.data.api.SchoolsService
import io.github.wulkanowy.data.db.AppDatabase
import io.github.wulkanowy.data.db.SharedPrefProvider
import io.github.wulkanowy.data.repositories.PreferencesRepository
-import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.AppInfo
-import io.github.wulkanowy.utils.RemoteConfigHelper
-import io.github.wulkanowy.utils.WebkitCookieManagerProxy
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.create
-import timber.log.Timber
import java.util.concurrent.TimeUnit
import javax.inject.Singleton
@@ -36,20 +32,6 @@ import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
internal class DataModule {
- @Singleton
- @Provides
- fun provideSdk(chuckerInterceptor: ChuckerInterceptor, remoteConfig: RemoteConfigHelper) =
- Sdk().apply {
- androidVersion = android.os.Build.VERSION.RELEASE
- buildTag = android.os.Build.MODEL
- userAgentTemplate = remoteConfig.userAgentTemplate
- setSimpleHttpLogger { Timber.d(it) }
- setAdditionalCookieManager(WebkitCookieManagerProxy())
-
- // for debug only
- addInterceptor(chuckerInterceptor, network = true)
- }
-
@Singleton
@Provides
fun provideChuckerCollector(
@@ -254,6 +236,10 @@ internal class DataModule {
@Provides
fun provideAdminMessageDao(database: AppDatabase) = database.adminMessagesDao
+ @Singleton
+ @Provides
+ fun provideMutesDao(database: AppDatabase) = database.mutedMessageSendersDao
+
@Singleton
@Provides
fun provideGradeDescriptiveDao(database: AppDatabase) = database.gradeDescriptiveDao
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
new file mode 100644
index 000000000..6d4f9edad
--- /dev/null
+++ b/app/src/main/java/io/github/wulkanowy/data/WulkanowySdkFactory.kt
@@ -0,0 +1,64 @@
+package io.github.wulkanowy.data
+
+import com.chuckerteam.chucker.api.ChuckerInterceptor
+import io.github.wulkanowy.data.db.entities.Semester
+import io.github.wulkanowy.data.db.entities.Student
+import io.github.wulkanowy.sdk.Sdk
+import io.github.wulkanowy.utils.RemoteConfigHelper
+import io.github.wulkanowy.utils.WebkitCookieManagerProxy
+import timber.log.Timber
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class WulkanowySdkFactory @Inject constructor(
+ private val chuckerInterceptor: ChuckerInterceptor,
+ private val remoteConfig: RemoteConfigHelper,
+ private val webkitCookieManagerProxy: WebkitCookieManagerProxy
+) {
+
+ private val sdk = Sdk().apply {
+ androidVersion = android.os.Build.VERSION.RELEASE
+ buildTag = android.os.Build.MODEL
+ userAgentTemplate = remoteConfig.userAgentTemplate
+ setSimpleHttpLogger { Timber.d(it) }
+ setAdditionalCookieManager(webkitCookieManagerProxy)
+
+ // for debug only
+ addInterceptor(chuckerInterceptor, network = true)
+ }
+
+ fun create() = sdk
+
+ fun create(student: Student, semester: Semester? = null): Sdk {
+ return create().apply {
+ email = student.email
+ password = student.password
+ symbol = student.symbol
+ schoolSymbol = student.schoolSymbol
+ studentId = student.studentId
+ classId = student.classId
+ emptyCookieJarInterceptor = true
+
+ if (Sdk.Mode.valueOf(student.loginMode) == Sdk.Mode.HEBE) {
+ mobileBaseUrl = student.mobileBaseUrl
+ } else {
+ scrapperBaseUrl = student.scrapperBaseUrl
+ domainSuffix = student.scrapperDomainSuffix
+ loginType = Sdk.ScrapperLoginType.valueOf(student.loginType)
+ }
+
+ mode = Sdk.Mode.valueOf(student.loginMode)
+ mobileBaseUrl = student.mobileBaseUrl
+ keyId = student.certificateKey
+ privatePem = student.privateKey
+
+ if (semester != null) {
+ diaryId = semester.diaryId
+ kindergartenDiaryId = semester.kindergartenDiaryId
+ schoolYear = semester.schoolYear
+ unitId = semester.unitId
+ }
+ }
+ }
+}
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 8e5841fe7..208daf75f 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
@@ -25,6 +25,7 @@ import io.github.wulkanowy.data.db.dao.MailboxDao
import io.github.wulkanowy.data.db.dao.MessageAttachmentDao
import io.github.wulkanowy.data.db.dao.MessagesDao
import io.github.wulkanowy.data.db.dao.MobileDeviceDao
+import io.github.wulkanowy.data.db.dao.MutedMessageSendersDao
import io.github.wulkanowy.data.db.dao.NoteDao
import io.github.wulkanowy.data.db.dao.NotificationDao
import io.github.wulkanowy.data.db.dao.RecipientDao
@@ -56,6 +57,7 @@ import io.github.wulkanowy.data.db.entities.Mailbox
import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.MessageAttachment
import io.github.wulkanowy.data.db.entities.MobileDevice
+import io.github.wulkanowy.data.db.entities.MutedMessageSender
import io.github.wulkanowy.data.db.entities.Note
import io.github.wulkanowy.data.db.entities.Notification
import io.github.wulkanowy.data.db.entities.Recipient
@@ -157,6 +159,7 @@ import javax.inject.Singleton
SchoolAnnouncement::class,
Notification::class,
AdminMessage::class,
+ MutedMessageSender::class,
GradeDescriptive::class,
],
autoMigrations = [
@@ -169,6 +172,8 @@ import javax.inject.Singleton
AutoMigration(from = 56, to = 57, spec = Migration57::class),
AutoMigration(from = 57, to = 58, spec = Migration58::class),
AutoMigration(from = 58, to = 59),
+ AutoMigration(from = 59, to = 60),
+ AutoMigration(from = 60, to = 61),
],
version = AppDatabase.VERSION_SCHEMA,
exportSchema = true
@@ -177,7 +182,7 @@ import javax.inject.Singleton
abstract class AppDatabase : RoomDatabase() {
companion object {
- const val VERSION_SCHEMA = 59
+ const val VERSION_SCHEMA = 61
fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf(
Migration2(),
@@ -303,5 +308,7 @@ abstract class AppDatabase : RoomDatabase() {
abstract val adminMessagesDao: AdminMessageDao
+ abstract val mutedMessageSendersDao: MutedMessageSendersDao
+
abstract val gradeDescriptiveDao: GradeDescriptiveDao
}
diff --git a/app/src/main/java/io/github/wulkanowy/data/db/dao/AdminMessageDao.kt b/app/src/main/java/io/github/wulkanowy/data/db/dao/AdminMessageDao.kt
index 2b4cb5975..6c8d7e471 100644
--- a/app/src/main/java/io/github/wulkanowy/data/db/dao/AdminMessageDao.kt
+++ b/app/src/main/java/io/github/wulkanowy/data/db/dao/AdminMessageDao.kt
@@ -2,24 +2,14 @@ package io.github.wulkanowy.data.db.dao
import androidx.room.Dao
import androidx.room.Query
-import androidx.room.Transaction
import io.github.wulkanowy.data.db.entities.AdminMessage
import kotlinx.coroutines.flow.Flow
import javax.inject.Singleton
@Singleton
@Dao
-abstract class AdminMessageDao : BaseDao {
+interface AdminMessageDao : BaseDao {
@Query("SELECT * FROM AdminMessages")
- abstract fun loadAll(): Flow>
-
- @Transaction
- open suspend fun removeOldAndSaveNew(
- oldMessages: List,
- newMessages: List
- ) {
- deleteAll(oldMessages)
- insertAll(newMessages)
- }
+ fun loadAll(): Flow>
}
diff --git a/app/src/main/java/io/github/wulkanowy/data/db/dao/BaseDao.kt b/app/src/main/java/io/github/wulkanowy/data/db/dao/BaseDao.kt
index 056a5cbd1..937e98248 100644
--- a/app/src/main/java/io/github/wulkanowy/data/db/dao/BaseDao.kt
+++ b/app/src/main/java/io/github/wulkanowy/data/db/dao/BaseDao.kt
@@ -3,6 +3,7 @@ package io.github.wulkanowy.data.db.dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
+import androidx.room.Transaction
import androidx.room.Update
interface BaseDao {
@@ -15,4 +16,10 @@ interface BaseDao {
@Delete
suspend fun deleteAll(items: List)
+
+ @Transaction
+ suspend fun removeOldAndSaveNew(oldItems: List, newItems: List) {
+ deleteAll(oldItems)
+ insertAll(newItems)
+ }
}
diff --git a/app/src/main/java/io/github/wulkanowy/data/db/dao/MessagesDao.kt b/app/src/main/java/io/github/wulkanowy/data/db/dao/MessagesDao.kt
index 1709f7636..11e6da1e7 100644
--- a/app/src/main/java/io/github/wulkanowy/data/db/dao/MessagesDao.kt
+++ b/app/src/main/java/io/github/wulkanowy/data/db/dao/MessagesDao.kt
@@ -5,15 +5,23 @@ import androidx.room.Query
import androidx.room.Transaction
import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.MessageWithAttachment
+import io.github.wulkanowy.data.db.entities.MessageWithMutedAuthor
import kotlinx.coroutines.flow.Flow
@Dao
interface MessagesDao : BaseDao {
-
@Transaction
@Query("SELECT * FROM Messages WHERE message_global_key = :messageGlobalKey")
fun loadMessageWithAttachment(messageGlobalKey: String): Flow
+ @Transaction
+ @Query("SELECT * FROM Messages WHERE mailbox_key = :mailboxKey AND folder_id = :folder ORDER BY date DESC")
+ fun loadMessagesWithMutedAuthor(mailboxKey: String, folder: Int): Flow>
+
+ @Transaction
+ @Query("SELECT * FROM Messages WHERE email = :email AND folder_id = :folder ORDER BY date DESC")
+ fun loadMessagesWithMutedAuthor(folder: Int, email: String): Flow>
+
@Query("SELECT * FROM Messages WHERE mailbox_key = :mailboxKey AND folder_id = :folder ORDER BY date DESC")
fun loadAll(mailboxKey: String, folder: Int): Flow>
diff --git a/app/src/main/java/io/github/wulkanowy/data/db/dao/MutedMessageSendersDao.kt b/app/src/main/java/io/github/wulkanowy/data/db/dao/MutedMessageSendersDao.kt
new file mode 100644
index 000000000..0a8664010
--- /dev/null
+++ b/app/src/main/java/io/github/wulkanowy/data/db/dao/MutedMessageSendersDao.kt
@@ -0,0 +1,20 @@
+package io.github.wulkanowy.data.db.dao
+
+import androidx.room.Dao
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
+import androidx.room.Query
+import io.github.wulkanowy.data.db.entities.MutedMessageSender
+
+@Dao
+interface MutedMessageSendersDao : BaseDao {
+
+ @Query("SELECT COUNT(*) FROM MutedMessageSenders WHERE author = :author")
+ suspend fun checkMute(author: String): Boolean
+
+ @Insert(onConflict = OnConflictStrategy.IGNORE)
+ suspend fun insertMute(mute: MutedMessageSender): Long
+
+ @Query("DELETE FROM MutedMessageSenders WHERE author = :author")
+ suspend fun deleteMute(author: String)
+}
diff --git a/app/src/main/java/io/github/wulkanowy/data/db/dao/TimetableDao.kt b/app/src/main/java/io/github/wulkanowy/data/db/dao/TimetableDao.kt
index b4b7379f2..40d97ea96 100644
--- a/app/src/main/java/io/github/wulkanowy/data/db/dao/TimetableDao.kt
+++ b/app/src/main/java/io/github/wulkanowy/data/db/dao/TimetableDao.kt
@@ -15,5 +15,5 @@ interface TimetableDao : BaseDao {
fun loadAll(diaryId: Int, studentId: Int, from: LocalDate, end: LocalDate): Flow>
@Query("SELECT * FROM Timetable WHERE diary_id = :diaryId AND student_id = :studentId AND date >= :from AND date <= :end")
- fun load(diaryId: Int, studentId: Int, from: LocalDate, end: LocalDate): List
+ suspend fun load(diaryId: Int, studentId: Int, from: LocalDate, end: LocalDate): List
}
diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/MessageWithAttachment.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/MessageWithAttachment.kt
index cd468215d..fc890e760 100644
--- a/app/src/main/java/io/github/wulkanowy/data/db/entities/MessageWithAttachment.kt
+++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/MessageWithAttachment.kt
@@ -2,11 +2,15 @@ package io.github.wulkanowy.data.db.entities
import androidx.room.Embedded
import androidx.room.Relation
+import java.io.Serializable
data class MessageWithAttachment(
@Embedded
val message: Message,
@Relation(parentColumn = "message_global_key", entityColumn = "message_global_key")
- val attachments: List
-)
+ val attachments: List,
+
+ @Relation(parentColumn = "correspondents", entityColumn = "author")
+ val mutedMessageSender: MutedMessageSender?,
+) : Serializable
diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/MessageWithMutedAuthor.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/MessageWithMutedAuthor.kt
new file mode 100644
index 000000000..e3cd1ca7d
--- /dev/null
+++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/MessageWithMutedAuthor.kt
@@ -0,0 +1,12 @@
+package io.github.wulkanowy.data.db.entities
+
+import androidx.room.Embedded
+import androidx.room.Relation
+
+data class MessageWithMutedAuthor(
+ @Embedded
+ val message: Message,
+
+ @Relation(parentColumn = "correspondents", entityColumn = "author")
+ val mutedMessageSender: MutedMessageSender?,
+)
diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/MutedMessageSender.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/MutedMessageSender.kt
new file mode 100644
index 000000000..f1770e64c
--- /dev/null
+++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/MutedMessageSender.kt
@@ -0,0 +1,15 @@
+package io.github.wulkanowy.data.db.entities
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+import java.io.Serializable
+
+@Entity(tableName = "MutedMessageSenders")
+data class MutedMessageSender(
+ @ColumnInfo(name = "author")
+ val author: String,
+) : Serializable {
+ @PrimaryKey(autoGenerate = true)
+ var id: Long = 0
+}
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 25e27ef18..ac096b02b 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
@@ -16,7 +16,9 @@ data class SchoolAnnouncement(
val subject: String,
- val content: String
+ val content: String,
+
+ val author: String? = null,
) : Serializable {
@PrimaryKey(autoGenerate = true)
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/MessageFolder.kt b/app/src/main/java/io/github/wulkanowy/data/enums/MessageFolder.kt
index 899ba9085..7cb4202a1 100644
--- a/app/src/main/java/io/github/wulkanowy/data/enums/MessageFolder.kt
+++ b/app/src/main/java/io/github/wulkanowy/data/enums/MessageFolder.kt
@@ -3,5 +3,10 @@ package io.github.wulkanowy.data.enums
enum class MessageFolder(val id: Int = 1) {
RECEIVED(1),
SENT(2),
- TRASHED(3)
+ TRASHED(3),
+ ;
+
+ companion object {
+ fun byId(id: Int) = entries.first { it.id == id }
+ }
}
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 16f1bbac0..85b37afc1 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
@@ -3,12 +3,26 @@ package io.github.wulkanowy.data.mappers
import io.github.wulkanowy.data.db.entities.SchoolAnnouncement
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.sdk.pojo.DirectorInformation as SdkDirectorInformation
+import io.github.wulkanowy.sdk.pojo.LastAnnouncement as SdkLastAnnouncement
+@JvmName("mapDirectorInformationToEntities")
fun List.mapToEntities(student: Student) = map {
SchoolAnnouncement(
userLoginId = student.userLoginId,
date = it.date,
subject = it.subject,
content = it.content,
+ author = null,
+ )
+}
+
+@JvmName("mapLastAnnouncementsToEntities")
+fun List.mapToEntities(student: Student) = map {
+ SchoolAnnouncement(
+ userLoginId = student.userLoginId,
+ date = it.date,
+ subject = it.subject,
+ content = it.content,
+ author = it.author,
)
}
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
index b831ee755..aa0022b08 100644
--- a/app/src/main/java/io/github/wulkanowy/data/repositories/AdminMessageRepository.kt
+++ b/app/src/main/java/io/github/wulkanowy/data/repositories/AdminMessageRepository.kt
@@ -6,6 +6,7 @@ import io.github.wulkanowy.data.db.dao.AdminMessageDao
import io.github.wulkanowy.data.db.entities.AdminMessage
import io.github.wulkanowy.data.networkBoundResource
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.sync.Mutex
import javax.inject.Inject
import javax.inject.Singleton
@@ -28,6 +29,6 @@ class AdminMessageRepository @Inject constructor(
saveFetchResult = { oldItems, newItems ->
adminMessageDao.removeOldAndSaveNew(oldItems, newItems)
},
- showSavedOnLoading = false,
)
+ .filterNot { it is Resource.Intermediate }
}
diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceRepository.kt
index 6d782047b..9b94cc103 100644
--- a/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceRepository.kt
+++ b/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceRepository.kt
@@ -1,5 +1,6 @@
package io.github.wulkanowy.data.repositories
+import io.github.wulkanowy.data.WulkanowySdkFactory
import io.github.wulkanowy.data.db.dao.AttendanceDao
import io.github.wulkanowy.data.db.dao.TimetableDao
import io.github.wulkanowy.data.db.entities.Attendance
@@ -7,19 +8,14 @@ import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.data.networkBoundResource
-import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.sdk.pojo.Absent
import io.github.wulkanowy.utils.AutoRefreshHelper
import io.github.wulkanowy.utils.getRefreshKey
-import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.monday
import io.github.wulkanowy.utils.sunday
-import io.github.wulkanowy.utils.switchSemester
import io.github.wulkanowy.utils.uniqueSubtract
-import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.sync.Mutex
-import kotlinx.coroutines.withContext
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime
@@ -30,7 +26,7 @@ import javax.inject.Singleton
class AttendanceRepository @Inject constructor(
private val attendanceDb: AttendanceDao,
private val timetableDb: TimetableDao,
- private val sdk: Sdk,
+ private val wulkanowySdkFactory: WulkanowySdkFactory,
private val refreshHelper: AutoRefreshHelper,
) {
@@ -58,23 +54,21 @@ class AttendanceRepository @Inject constructor(
attendanceDb.loadAll(semester.diaryId, semester.studentId, start.monday, end.sunday)
},
fetch = {
- val lessons = withContext(Dispatchers.IO) {
- timetableDb.load(
- semester.diaryId, semester.studentId, start.monday, end.sunday
- )
- }
- sdk.init(student)
- .switchSemester(semester)
+ val lessons = timetableDb.load(
+ semester.diaryId, semester.studentId, start.monday, end.sunday
+ )
+ wulkanowySdkFactory.create(student, semester)
.getAttendance(start.monday, end.sunday)
.mapToEntities(semester, lessons)
},
saveFetchResult = { old, new ->
- attendanceDb.deleteAll(old uniqueSubtract new)
val attendanceToAdd = (new uniqueSubtract old).map { newAttendance ->
newAttendance.apply { if (notify) isNotified = false }
}
- attendanceDb.insertAll(attendanceToAdd)
-
+ attendanceDb.removeOldAndSaveNew(
+ oldItems = old uniqueSubtract new,
+ newItems = attendanceToAdd,
+ )
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester, start, end))
},
filterResult = { it.filter { item -> item.date in start..end } }
@@ -93,8 +87,10 @@ class AttendanceRepository @Inject constructor(
}
suspend fun excuseForAbsence(
- student: Student, semester: Semester,
- absenceList: List, reason: String? = null
+ student: Student,
+ semester: Semester,
+ absenceList: List,
+ reason: String? = null
) {
val items = absenceList.map { attendance ->
Absent(
@@ -102,8 +98,7 @@ class AttendanceRepository @Inject constructor(
timeId = attendance.timeId
)
}
- sdk.init(student)
- .switchSemester(semester)
+ wulkanowySdkFactory.create(student, semester)
.excuseForAbsence(items, reason)
}
}
diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceSummaryRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceSummaryRepository.kt
index 6bdcf9d7f..78c98169b 100644
--- a/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceSummaryRepository.kt
+++ b/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceSummaryRepository.kt
@@ -1,15 +1,15 @@
package io.github.wulkanowy.data.repositories
+import androidx.room.withTransaction
+import io.github.wulkanowy.data.WulkanowySdkFactory
+import io.github.wulkanowy.data.db.AppDatabase
import io.github.wulkanowy.data.db.dao.AttendanceSummaryDao
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.data.networkBoundResource
-import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.AutoRefreshHelper
import io.github.wulkanowy.utils.getRefreshKey
-import io.github.wulkanowy.utils.init
-import io.github.wulkanowy.utils.switchSemester
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.sync.Mutex
import javax.inject.Inject
@@ -18,8 +18,9 @@ import javax.inject.Singleton
@Singleton
class AttendanceSummaryRepository @Inject constructor(
private val attendanceDb: AttendanceSummaryDao,
- private val sdk: Sdk,
private val refreshHelper: AutoRefreshHelper,
+ private val appDatabase: AppDatabase,
+ private val wulkanowySdkFactory: WulkanowySdkFactory,
) {
private val saveFetchResultMutex = Mutex()
@@ -40,14 +41,15 @@ class AttendanceSummaryRepository @Inject constructor(
},
query = { attendanceDb.loadAll(semester.diaryId, semester.studentId, subjectId) },
fetch = {
- sdk.init(student)
- .switchSemester(semester)
+ wulkanowySdkFactory.create(student, semester)
.getAttendanceSummary(subjectId)
.mapToEntities(semester, subjectId)
},
saveFetchResult = { old, new ->
- attendanceDb.deleteAll(old uniqueSubtract new)
- attendanceDb.insertAll(new uniqueSubtract old)
+ appDatabase.withTransaction {
+ attendanceDb.deleteAll(old uniqueSubtract new)
+ attendanceDb.insertAll(new uniqueSubtract old)
+ }
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester))
}
)
diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/CompletedLessonsRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/CompletedLessonsRepository.kt
index 1579ae62b..45a36f55c 100644
--- a/app/src/main/java/io/github/wulkanowy/data/repositories/CompletedLessonsRepository.kt
+++ b/app/src/main/java/io/github/wulkanowy/data/repositories/CompletedLessonsRepository.kt
@@ -1,12 +1,16 @@
package io.github.wulkanowy.data.repositories
+import io.github.wulkanowy.data.WulkanowySdkFactory
import io.github.wulkanowy.data.db.dao.CompletedLessonsDao
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.data.networkBoundResource
-import io.github.wulkanowy.sdk.Sdk
-import io.github.wulkanowy.utils.*
+import io.github.wulkanowy.utils.AutoRefreshHelper
+import io.github.wulkanowy.utils.getRefreshKey
+import io.github.wulkanowy.utils.monday
+import io.github.wulkanowy.utils.sunday
+import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.sync.Mutex
import java.time.LocalDate
import javax.inject.Inject
@@ -15,7 +19,7 @@ import javax.inject.Singleton
@Singleton
class CompletedLessonsRepository @Inject constructor(
private val completedLessonsDb: CompletedLessonsDao,
- private val sdk: Sdk,
+ private val wulkanowySdkFactory: WulkanowySdkFactory,
private val refreshHelper: AutoRefreshHelper,
) {
@@ -47,14 +51,15 @@ class CompletedLessonsRepository @Inject constructor(
)
},
fetch = {
- sdk.init(student)
- .switchSemester(semester)
+ wulkanowySdkFactory.create(student, semester)
.getCompletedLessons(start.monday, end.sunday)
.mapToEntities(semester)
},
saveFetchResult = { old, new ->
- completedLessonsDb.deleteAll(old uniqueSubtract new)
- completedLessonsDb.insertAll(new uniqueSubtract old)
+ completedLessonsDb.removeOldAndSaveNew(
+ oldItems = old uniqueSubtract new,
+ newItems = new uniqueSubtract old,
+ )
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester, start, end))
},
filterResult = { it.filter { item -> item.date in start..end } }
diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/ConferenceRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/ConferenceRepository.kt
index 7eb37f0b7..58ce0091a 100644
--- a/app/src/main/java/io/github/wulkanowy/data/repositories/ConferenceRepository.kt
+++ b/app/src/main/java/io/github/wulkanowy/data/repositories/ConferenceRepository.kt
@@ -1,16 +1,14 @@
package io.github.wulkanowy.data.repositories
+import io.github.wulkanowy.data.WulkanowySdkFactory
import io.github.wulkanowy.data.db.dao.ConferenceDao
import io.github.wulkanowy.data.db.entities.Conference
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.data.networkBoundResource
-import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.AutoRefreshHelper
import io.github.wulkanowy.utils.getRefreshKey
-import io.github.wulkanowy.utils.init
-import io.github.wulkanowy.utils.switchSemester
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.sync.Mutex
@@ -21,7 +19,7 @@ import javax.inject.Singleton
@Singleton
class ConferenceRepository @Inject constructor(
private val conferenceDb: ConferenceDao,
- private val sdk: Sdk,
+ private val wulkanowySdkFactory: WulkanowySdkFactory,
private val refreshHelper: AutoRefreshHelper,
) {
@@ -46,19 +44,18 @@ class ConferenceRepository @Inject constructor(
conferenceDb.loadAll(semester.diaryId, student.studentId, startDate)
},
fetch = {
- sdk.init(student)
- .switchSemester(semester)
+ wulkanowySdkFactory.create(student, semester)
.getConferences()
.mapToEntities(semester)
.filter { it.date >= startDate }
},
saveFetchResult = { old, new ->
- val conferencesToSave = (new uniqueSubtract old).onEach {
- if (notify) it.isNotified = false
- }
-
- conferenceDb.deleteAll(old uniqueSubtract new)
- conferenceDb.insertAll(conferencesToSave)
+ conferenceDb.removeOldAndSaveNew(
+ oldItems = old uniqueSubtract new,
+ newItems = (new uniqueSubtract old).onEach {
+ if (notify) it.isNotified = false
+ },
+ )
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester))
}
)
diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/ExamRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/ExamRepository.kt
index 96026a55b..89dbcd5ce 100644
--- a/app/src/main/java/io/github/wulkanowy/data/repositories/ExamRepository.kt
+++ b/app/src/main/java/io/github/wulkanowy/data/repositories/ExamRepository.kt
@@ -1,18 +1,16 @@
package io.github.wulkanowy.data.repositories
+import io.github.wulkanowy.data.WulkanowySdkFactory
import io.github.wulkanowy.data.db.dao.ExamDao
import io.github.wulkanowy.data.db.entities.Exam
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.data.networkBoundResource
-import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.AutoRefreshHelper
import io.github.wulkanowy.utils.endExamsDay
import io.github.wulkanowy.utils.getRefreshKey
-import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.startExamsDay
-import io.github.wulkanowy.utils.switchSemester
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.sync.Mutex
@@ -23,7 +21,7 @@ import javax.inject.Singleton
@Singleton
class ExamRepository @Inject constructor(
private val examDb: ExamDao,
- private val sdk: Sdk,
+ private val wulkanowySdkFactory: WulkanowySdkFactory,
private val refreshHelper: AutoRefreshHelper,
) {
@@ -56,18 +54,17 @@ class ExamRepository @Inject constructor(
)
},
fetch = {
- sdk.init(student)
- .switchSemester(semester)
+ wulkanowySdkFactory.create(student, semester)
.getExams(start.startExamsDay, start.endExamsDay)
.mapToEntities(semester)
},
saveFetchResult = { old, new ->
- val examsToSave = (new uniqueSubtract old).onEach {
- if (notify) it.isNotified = false
- }
-
- examDb.deleteAll(old uniqueSubtract new)
- examDb.insertAll(examsToSave)
+ examDb.removeOldAndSaveNew(
+ oldItems = old uniqueSubtract new,
+ newItems = (new uniqueSubtract old).onEach {
+ if (notify) it.isNotified = false
+ },
+ )
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester, start, end))
},
filterResult = { it.filter { item -> item.date in start..end } }
diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/GradeRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/GradeRepository.kt
index 1e2ea9354..e899f900d 100644
--- a/app/src/main/java/io/github/wulkanowy/data/repositories/GradeRepository.kt
+++ b/app/src/main/java/io/github/wulkanowy/data/repositories/GradeRepository.kt
@@ -1,5 +1,6 @@
package io.github.wulkanowy.data.repositories
+import io.github.wulkanowy.data.WulkanowySdkFactory
import io.github.wulkanowy.data.db.dao.GradeDao
import io.github.wulkanowy.data.db.dao.GradeDescriptiveDao
import io.github.wulkanowy.data.db.dao.GradeSummaryDao
@@ -10,11 +11,8 @@ import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.data.networkBoundResource
-import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.AutoRefreshHelper
import io.github.wulkanowy.utils.getRefreshKey
-import io.github.wulkanowy.utils.init
-import io.github.wulkanowy.utils.switchSemester
import io.github.wulkanowy.utils.toLocalDate
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.flow.Flow
@@ -30,7 +28,7 @@ class GradeRepository @Inject constructor(
private val gradeDb: GradeDao,
private val gradeSummaryDb: GradeSummaryDao,
private val gradeDescriptiveDb: GradeDescriptiveDao,
- private val sdk: Sdk,
+ private val wulkanowySdkFactory: WulkanowySdkFactory,
private val refreshHelper: AutoRefreshHelper,
) {
@@ -63,8 +61,7 @@ class GradeRepository @Inject constructor(
}
},
fetch = {
- val (details, summary, descriptive) = sdk.init(student)
- .switchSemester(semester)
+ val (details, summary, descriptive) = wulkanowySdkFactory.create(student, semester)
.getGrades(semester.semesterId)
Triple(
@@ -87,10 +84,12 @@ class GradeRepository @Inject constructor(
new: List,
notify: Boolean
) {
- gradeDescriptiveDb.deleteAll(old uniqueSubtract new)
- gradeDescriptiveDb.insertAll((new uniqueSubtract old).onEach {
- if (notify) it.isNotified = false
- })
+ gradeDescriptiveDb.removeOldAndSaveNew(
+ oldItems = old uniqueSubtract new,
+ newItems = (new uniqueSubtract old).onEach {
+ if (notify) it.isNotified = false
+ },
+ )
}
private suspend fun refreshGradeDetails(
@@ -101,13 +100,16 @@ class GradeRepository @Inject constructor(
) {
val notifyBreakDate = oldGrades.maxByOrNull { it.date }?.date
?: student.registrationDate.toLocalDate()
- gradeDb.deleteAll(oldGrades uniqueSubtract newDetails)
- gradeDb.insertAll((newDetails uniqueSubtract oldGrades).onEach {
- if (it.date >= notifyBreakDate) it.apply {
- isRead = false
- if (notify) isNotified = false
- }
- })
+
+ gradeDb.removeOldAndSaveNew(
+ oldItems = oldGrades uniqueSubtract newDetails,
+ newItems = (newDetails uniqueSubtract oldGrades).onEach {
+ if (it.date >= notifyBreakDate) it.apply {
+ isRead = false
+ if (notify) isNotified = false
+ }
+ },
+ )
}
private suspend fun refreshGradeSummaries(
@@ -115,31 +117,43 @@ class GradeRepository @Inject constructor(
newSummary: List,
notify: Boolean
) {
- gradeSummaryDb.deleteAll(oldSummaries uniqueSubtract newSummary)
- gradeSummaryDb.insertAll((newSummary uniqueSubtract oldSummaries).onEach { summary ->
- val oldSummary = oldSummaries.find { old -> old.subject == summary.subject }
- summary.isPredictedGradeNotified = when {
- summary.predictedGrade.isEmpty() -> true
- notify && oldSummary?.predictedGrade != summary.predictedGrade -> false
- else -> true
- }
- summary.isFinalGradeNotified = when {
- summary.finalGrade.isEmpty() -> true
- notify && oldSummary?.finalGrade != summary.finalGrade -> false
- else -> true
- }
+ gradeSummaryDb.removeOldAndSaveNew(
+ oldItems = oldSummaries uniqueSubtract newSummary,
+ newItems = (newSummary uniqueSubtract oldSummaries).onEach { summary ->
+ getGradeSummaryWithUpdatedNotificationState(
+ summary = summary,
+ oldSummary = oldSummaries.find { it.subject == summary.subject },
+ notify = notify,
+ )
+ },
+ )
+ }
- summary.predictedGradeLastChange = when {
- oldSummary == null -> Instant.now()
- summary.predictedGrade != oldSummary.predictedGrade -> Instant.now()
- else -> oldSummary.predictedGradeLastChange
- }
- summary.finalGradeLastChange = when {
- oldSummary == null -> Instant.now()
- summary.finalGrade != oldSummary.finalGrade -> Instant.now()
- else -> oldSummary.finalGradeLastChange
- }
- })
+ private fun getGradeSummaryWithUpdatedNotificationState(
+ summary: GradeSummary,
+ oldSummary: GradeSummary?,
+ notify: Boolean,
+ ) {
+ summary.isPredictedGradeNotified = when {
+ summary.predictedGrade.isEmpty() -> true
+ notify && oldSummary?.predictedGrade != summary.predictedGrade -> false
+ else -> true
+ }
+ summary.isFinalGradeNotified = when {
+ summary.finalGrade.isEmpty() -> true
+ notify && oldSummary?.finalGrade != summary.finalGrade -> false
+ else -> true
+ }
+ summary.predictedGradeLastChange = when {
+ oldSummary == null -> Instant.now()
+ summary.predictedGrade != oldSummary.predictedGrade -> Instant.now()
+ else -> oldSummary.predictedGradeLastChange
+ }
+ summary.finalGradeLastChange = when {
+ oldSummary == null -> Instant.now()
+ summary.finalGrade != oldSummary.finalGrade -> Instant.now()
+ else -> oldSummary.finalGradeLastChange
+ }
}
fun getUnreadGrades(semester: Semester): Flow> {
diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/GradeStatisticsRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/GradeStatisticsRepository.kt
index 23d7b8582..f120d34f3 100644
--- a/app/src/main/java/io/github/wulkanowy/data/repositories/GradeStatisticsRepository.kt
+++ b/app/src/main/java/io/github/wulkanowy/data/repositories/GradeStatisticsRepository.kt
@@ -1,5 +1,6 @@
package io.github.wulkanowy.data.repositories
+import io.github.wulkanowy.data.WulkanowySdkFactory
import io.github.wulkanowy.data.db.dao.GradePartialStatisticsDao
import io.github.wulkanowy.data.db.dao.GradePointsStatisticsDao
import io.github.wulkanowy.data.db.dao.GradeSemesterStatisticsDao
@@ -12,14 +13,11 @@ import io.github.wulkanowy.data.mappers.mapPointsToStatisticsItems
import io.github.wulkanowy.data.mappers.mapSemesterToStatisticItems
import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.data.networkBoundResource
-import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.AutoRefreshHelper
import io.github.wulkanowy.utils.getRefreshKey
-import io.github.wulkanowy.utils.init
-import io.github.wulkanowy.utils.switchSemester
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.sync.Mutex
-import java.util.*
+import java.util.Locale
import javax.inject.Inject
import javax.inject.Singleton
@@ -28,7 +26,7 @@ class GradeStatisticsRepository @Inject constructor(
private val gradePartialStatisticsDb: GradePartialStatisticsDao,
private val gradePointsStatisticsDb: GradePointsStatisticsDao,
private val gradeSemesterStatisticsDb: GradeSemesterStatisticsDao,
- private val sdk: Sdk,
+ private val wulkanowySdkFactory: WulkanowySdkFactory,
private val refreshHelper: AutoRefreshHelper,
) {
@@ -56,14 +54,15 @@ class GradeStatisticsRepository @Inject constructor(
},
query = { gradePartialStatisticsDb.loadAll(semester.semesterId, semester.studentId) },
fetch = {
- sdk.init(student)
- .switchSemester(semester)
+ wulkanowySdkFactory.create(student, semester)
.getGradesPartialStatistics(semester.semesterId)
.mapToEntities(semester)
},
saveFetchResult = { old, new ->
- gradePartialStatisticsDb.deleteAll(old uniqueSubtract new)
- gradePartialStatisticsDb.insertAll(new uniqueSubtract old)
+ gradePartialStatisticsDb.removeOldAndSaveNew(
+ oldItems = old uniqueSubtract new,
+ newItems = new uniqueSubtract old,
+ )
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(partialCacheKey, semester))
},
mapResult = { items ->
@@ -80,6 +79,7 @@ class GradeStatisticsRepository @Inject constructor(
)
listOf(summaryItem) + items
}
+
else -> items.filter { it.subject == subjectName }
}.mapPartialToStatisticItems()
}
@@ -101,14 +101,15 @@ class GradeStatisticsRepository @Inject constructor(
},
query = { gradeSemesterStatisticsDb.loadAll(semester.semesterId, semester.studentId) },
fetch = {
- sdk.init(student)
- .switchSemester(semester)
+ wulkanowySdkFactory.create(student, semester)
.getGradesSemesterStatistics(semester.semesterId)
.mapToEntities(semester)
},
saveFetchResult = { old, new ->
- gradeSemesterStatisticsDb.deleteAll(old uniqueSubtract new)
- gradeSemesterStatisticsDb.insertAll(new uniqueSubtract old)
+ gradeSemesterStatisticsDb.removeOldAndSaveNew(
+ oldItems = old uniqueSubtract new,
+ newItems = new uniqueSubtract old,
+ )
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(semesterCacheKey, semester))
},
mapResult = { items ->
@@ -138,6 +139,7 @@ class GradeStatisticsRepository @Inject constructor(
}
listOf(summaryItem) + itemsWithAverage
}
+
else -> itemsWithAverage.filter { it.subject == subjectName }
}.mapSemesterToStatisticItems()
}
@@ -157,14 +159,15 @@ class GradeStatisticsRepository @Inject constructor(
},
query = { gradePointsStatisticsDb.loadAll(semester.semesterId, semester.studentId) },
fetch = {
- sdk.init(student)
- .switchSemester(semester)
+ wulkanowySdkFactory.create(student, semester)
.getGradesPointsStatistics(semester.semesterId)
.mapToEntities(semester)
},
saveFetchResult = { old, new ->
- gradePointsStatisticsDb.deleteAll(old uniqueSubtract new)
- gradePointsStatisticsDb.insertAll(new uniqueSubtract old)
+ gradePointsStatisticsDb.removeOldAndSaveNew(
+ oldItems = old uniqueSubtract new,
+ newItems = new uniqueSubtract old,
+ )
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(pointsCacheKey, semester))
},
mapResult = { items ->
diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/HomeworkRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/HomeworkRepository.kt
index 010cf8458..7893ef631 100644
--- a/app/src/main/java/io/github/wulkanowy/data/repositories/HomeworkRepository.kt
+++ b/app/src/main/java/io/github/wulkanowy/data/repositories/HomeworkRepository.kt
@@ -1,18 +1,16 @@
package io.github.wulkanowy.data.repositories
+import io.github.wulkanowy.data.WulkanowySdkFactory
import io.github.wulkanowy.data.db.dao.HomeworkDao
import io.github.wulkanowy.data.db.entities.Homework
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.data.networkBoundResource
-import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.AutoRefreshHelper
import io.github.wulkanowy.utils.getRefreshKey
-import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.monday
import io.github.wulkanowy.utils.sunday
-import io.github.wulkanowy.utils.switchSemester
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.sync.Mutex
import java.time.LocalDate
@@ -22,7 +20,7 @@ import javax.inject.Singleton
@Singleton
class HomeworkRepository @Inject constructor(
private val homeworkDb: HomeworkDao,
- private val sdk: Sdk,
+ private val wulkanowySdkFactory: WulkanowySdkFactory,
private val refreshHelper: AutoRefreshHelper,
) {
@@ -55,20 +53,19 @@ class HomeworkRepository @Inject constructor(
)
},
fetch = {
- sdk.init(student)
- .switchSemester(semester)
+ wulkanowySdkFactory.create(student, semester)
.getHomework(start.monday, end.sunday)
.mapToEntities(semester)
},
saveFetchResult = { old, new ->
- val homeWorkToSave = (new uniqueSubtract old).onEach {
- if (notify) it.isNotified = false
- }
val filteredOld = old.filterNot { it.isAddedByUser }
- homeworkDb.deleteAll(filteredOld uniqueSubtract new)
- homeworkDb.insertAll(homeWorkToSave)
-
+ homeworkDb.removeOldAndSaveNew(
+ oldItems = filteredOld uniqueSubtract new,
+ newItems = (new uniqueSubtract old).onEach {
+ if (notify) it.isNotified = false
+ },
+ )
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester, start, end))
}
)
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 4ff4517d0..3636cb51e 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
@@ -1,12 +1,11 @@
package io.github.wulkanowy.data.repositories
+import io.github.wulkanowy.data.WulkanowySdkFactory
import io.github.wulkanowy.data.db.dao.LuckyNumberDao
import io.github.wulkanowy.data.db.entities.LuckyNumber
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.mappers.mapToEntity
import io.github.wulkanowy.data.networkBoundResource
-import io.github.wulkanowy.sdk.Sdk
-import io.github.wulkanowy.utils.init
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.sync.Mutex
@@ -18,7 +17,7 @@ import javax.inject.Singleton
@Singleton
class LuckyNumberRepository @Inject constructor(
private val luckyNumberDb: LuckyNumberDao,
- private val sdk: Sdk
+ private val wulkanowySdkFactory: WulkanowySdkFactory,
) {
private val saveFetchResultMutex = Mutex()
@@ -33,17 +32,18 @@ class LuckyNumberRepository @Inject constructor(
shouldFetch = { it == null || forceRefresh },
query = { luckyNumberDb.load(student.studentId, now()) },
fetch = {
- sdk.init(student).getLuckyNumber(student.schoolShortName)?.mapToEntity(student)
+ wulkanowySdkFactory.create(student)
+ .getLuckyNumber(student.schoolShortName)
+ ?.mapToEntity(student)
},
saveFetchResult = { oldLuckyNumber, newLuckyNumber ->
newLuckyNumber ?: return@networkBoundResource
if (newLuckyNumber != oldLuckyNumber) {
- val updatedLuckNumberList =
- listOf(newLuckyNumber.apply { if (notify) isNotified = false })
-
- oldLuckyNumber?.let { luckyNumberDb.deleteAll(listOfNotNull(it)) }
- luckyNumberDb.insertAll(updatedLuckNumberList)
+ luckyNumberDb.removeOldAndSaveNew(
+ oldItems = listOfNotNull(oldLuckyNumber),
+ newItems = listOf(newLuckyNumber.apply { if (notify) isNotified = false }),
+ )
}
}
)
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 c8fccb23d..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
@@ -4,17 +4,22 @@ import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R
import io.github.wulkanowy.data.Resource
+import io.github.wulkanowy.data.WulkanowySdkFactory
import io.github.wulkanowy.data.db.SharedPrefProvider
import io.github.wulkanowy.data.db.dao.MailboxDao
import io.github.wulkanowy.data.db.dao.MessageAttachmentDao
import io.github.wulkanowy.data.db.dao.MessagesDao
+import io.github.wulkanowy.data.db.dao.MutedMessageSendersDao
import io.github.wulkanowy.data.db.entities.Mailbox
import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.MessageWithAttachment
+import io.github.wulkanowy.data.db.entities.MessageWithMutedAuthor
+import io.github.wulkanowy.data.db.entities.MutedMessageSender
import io.github.wulkanowy.data.db.entities.Recipient
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.enums.MessageFolder
import io.github.wulkanowy.data.enums.MessageFolder.RECEIVED
+import io.github.wulkanowy.data.enums.MessageFolder.SENT
import io.github.wulkanowy.data.enums.MessageFolder.TRASHED
import io.github.wulkanowy.data.mappers.mapFromEntities
import io.github.wulkanowy.data.mappers.mapToEntities
@@ -22,16 +27,14 @@ import io.github.wulkanowy.data.networkBoundResource
import io.github.wulkanowy.data.onResourceError
import io.github.wulkanowy.data.onResourceSuccess
import io.github.wulkanowy.data.pojos.MessageDraft
+import io.github.wulkanowy.data.toFirstResult
import io.github.wulkanowy.data.waitForResult
import io.github.wulkanowy.domain.messages.GetMailboxByStudentUseCase
-import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.sdk.pojo.Folder
import io.github.wulkanowy.utils.AutoRefreshHelper
import io.github.wulkanowy.utils.getRefreshKey
-import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.first
import kotlinx.coroutines.sync.Mutex
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
@@ -42,8 +45,9 @@ import javax.inject.Singleton
@Singleton
class MessageRepository @Inject constructor(
private val messagesDb: MessagesDao,
+ private val mutedMessageSendersDao: MutedMessageSendersDao,
private val messageAttachmentDao: MessageAttachmentDao,
- private val sdk: Sdk,
+ private val wulkanowySdkFactory: WulkanowySdkFactory,
@ApplicationContext private val context: Context,
private val refreshHelper: AutoRefreshHelper,
private val sharedPrefProvider: SharedPrefProvider,
@@ -51,7 +55,6 @@ class MessageRepository @Inject constructor(
private val mailboxDao: MailboxDao,
private val getMailboxByStudentUseCase: GetMailboxByStudentUseCase,
) {
-
private val saveFetchResultMutex = Mutex()
private val messagesCacheKey = "message"
@@ -63,7 +66,7 @@ class MessageRepository @Inject constructor(
folder: MessageFolder,
forceRefresh: Boolean,
notify: Boolean = false,
- ): Flow>> = networkBoundResource(
+ ): Flow>> = networkBoundResource(
mutex = saveFetchResultMutex,
isResultEmpty = { it.isEmpty() },
shouldFetch = {
@@ -74,21 +77,30 @@ class MessageRepository @Inject constructor(
},
query = {
if (mailbox == null) {
- messagesDb.loadAll(folder.id, student.email)
- } else messagesDb.loadAll(mailbox.globalKey, folder.id)
+ messagesDb.loadMessagesWithMutedAuthor(folder.id, student.email)
+ } else messagesDb.loadMessagesWithMutedAuthor(mailbox.globalKey, folder.id)
},
fetch = {
- sdk.init(student).getMessages(
- folder = Folder.valueOf(folder.name),
- mailboxKey = mailbox?.globalKey,
- ).mapToEntities(student, mailbox, mailboxDao.loadAll(student.email))
+ wulkanowySdkFactory.create(student)
+ .getMessages(
+ folder = Folder.valueOf(folder.name),
+ mailboxKey = mailbox?.globalKey,
+ )
+ .mapToEntities(
+ student = student,
+ mailbox = mailbox,
+ allMailboxes = mailboxDao.loadAll(student.email)
+ )
},
- saveFetchResult = { old, new ->
- messagesDb.deleteAll(old uniqueSubtract new)
- messagesDb.insertAll((new uniqueSubtract old).onEach {
- it.isNotified = !notify
- })
-
+ saveFetchResult = { oldWithAuthors, new ->
+ val old = oldWithAuthors.map { it.message }
+ messagesDb.removeOldAndSaveNew(
+ oldItems = old uniqueSubtract new,
+ newItems = (new uniqueSubtract old).onEach {
+ val muted = isMuted(it.correspondents)
+ it.isNotified = !notify || muted
+ },
+ )
refreshHelper.updateLastRefreshTimestamp(
getRefreshKey(messagesCacheKey, mailbox, folder)
)
@@ -106,14 +118,13 @@ class MessageRepository @Inject constructor(
Timber.d("Message content in db empty: ${it.message.content.isBlank()}")
(it.message.unread && markAsRead) || it.message.content.isBlank()
},
- query = {
- messagesDb.loadMessageWithAttachment(message.messageGlobalKey)
- },
+ query = { messagesDb.loadMessageWithAttachment(message.messageGlobalKey) },
fetch = {
- sdk.init(student).getMessageDetails(
- messageKey = it!!.message.messageGlobalKey,
- markAsRead = message.unread && markAsRead,
- )
+ wulkanowySdkFactory.create(student)
+ .getMessageDetails(
+ messageKey = message.messageGlobalKey,
+ markAsRead = message.unread && markAsRead,
+ )
},
saveFetchResult = { old, new ->
checkNotNull(old) { "Fetched message no longer exist!" }
@@ -152,22 +163,36 @@ class MessageRepository @Inject constructor(
subject: String,
content: String,
recipients: List,
- mailboxId: String,
+ mailbox: Mailbox,
) {
- sdk.init(student).sendMessage(
- subject = subject,
- content = content,
- recipients = recipients.mapFromEntities(),
- mailboxId = mailboxId,
- )
+ wulkanowySdkFactory.create(student)
+ .sendMessage(
+ subject = subject,
+ content = content,
+ recipients = recipients.mapFromEntities(),
+ mailboxId = mailbox.globalKey,
+ )
+ refreshFolders(student, mailbox, listOf(SENT))
}
- suspend fun deleteMessages(student: Student, mailbox: Mailbox?, messages: List) {
+ suspend fun restoreMessages(student: Student, mailbox: Mailbox?, messages: List) {
+ wulkanowySdkFactory.create(student)
+ .restoreMessages(messages = messages.map { it.messageGlobalKey })
+
+ refreshFolders(student, mailbox)
+ }
+
+ suspend fun deleteMessage(student: Student, message: Message) {
+ deleteMessages(student, listOf(message))
+ }
+
+ suspend fun deleteMessages(student: Student, messages: List) {
val firstMessage = messages.first()
- sdk.init(student).deleteMessages(
- messages = messages.map { it.messageGlobalKey },
- removeForever = firstMessage.folderId == TRASHED.id,
- )
+ wulkanowySdkFactory.create(student)
+ .deleteMessages(
+ messages = messages.map { it.messageGlobalKey },
+ removeForever = firstMessage.folderId == TRASHED.id,
+ )
if (firstMessage.folderId != TRASHED.id) {
val deletedMessages = messages.map {
@@ -181,18 +206,24 @@ class MessageRepository @Inject constructor(
}
messagesDb.updateAll(deletedMessages)
- } else messagesDb.deleteAll(messages)
-
- getMessages(
- student = student,
- mailbox = mailbox,
- folder = TRASHED,
- forceRefresh = true,
- ).first()
+ } else {
+ messagesDb.deleteAll(messages)
+ }
}
- suspend fun deleteMessage(student: Student, mailbox: Mailbox?, message: Message) {
- deleteMessages(student, mailbox, listOf(message))
+ private suspend fun refreshFolders(
+ student: Student,
+ mailbox: Mailbox?,
+ folders: List = MessageFolder.entries
+ ) {
+ folders.forEach {
+ getMessages(
+ student = student,
+ mailbox = mailbox,
+ folder = it,
+ forceRefresh = true,
+ ).toFirstResult()
+ }
}
suspend fun getMailboxes(student: Student, forceRefresh: Boolean) = networkBoundResource(
@@ -206,7 +237,9 @@ class MessageRepository @Inject constructor(
},
query = { mailboxDao.loadAll(student.email, student.symbol, student.schoolSymbol) },
fetch = {
- sdk.init(student).getMailboxes().mapToEntities(student)
+ wulkanowySdkFactory.create(student)
+ .getMailboxes()
+ .mapToEntities(student)
},
saveFetchResult = { old, new ->
mailboxDao.deleteAll(old uniqueSubtract new)
@@ -236,4 +269,18 @@ class MessageRepository @Inject constructor(
context.getString(R.string.pref_key_message_draft),
value?.let { json.encodeToString(it) }
)
+
+ private suspend fun isMuted(author: String): Boolean {
+ return mutedMessageSendersDao.checkMute(author)
+ }
+
+ suspend fun muteMessage(author: String) {
+ if (isMuted(author)) return
+ mutedMessageSendersDao.insertMute(MutedMessageSender(author))
+ }
+
+ suspend fun unmuteMessage(author: String) {
+ if (!isMuted(author)) return
+ mutedMessageSendersDao.deleteMute(author)
+ }
}
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 412f9e7f0..19466554a 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
@@ -1,5 +1,6 @@
package io.github.wulkanowy.data.repositories
+import io.github.wulkanowy.data.WulkanowySdkFactory
import io.github.wulkanowy.data.db.dao.MobileDeviceDao
import io.github.wulkanowy.data.db.entities.MobileDevice
import io.github.wulkanowy.data.db.entities.Semester
@@ -8,11 +9,8 @@ import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.data.mappers.mapToMobileDeviceToken
import io.github.wulkanowy.data.networkBoundResource
import io.github.wulkanowy.data.pojos.MobileDeviceToken
-import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.AutoRefreshHelper
import io.github.wulkanowy.utils.getRefreshKey
-import io.github.wulkanowy.utils.init
-import io.github.wulkanowy.utils.switchSemester
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.sync.Mutex
import javax.inject.Inject
@@ -21,7 +19,7 @@ import javax.inject.Singleton
@Singleton
class MobileDeviceRepository @Inject constructor(
private val mobileDb: MobileDeviceDao,
- private val sdk: Sdk,
+ private val wulkanowySdkFactory: WulkanowySdkFactory,
private val refreshHelper: AutoRefreshHelper,
) {
@@ -42,30 +40,28 @@ class MobileDeviceRepository @Inject constructor(
},
query = { mobileDb.loadAll(student.userLoginId) },
fetch = {
- sdk.init(student)
- .switchSemester(semester)
+ wulkanowySdkFactory.create(student, semester)
.getRegisteredDevices()
.mapToEntities(student)
},
saveFetchResult = { old, new ->
- mobileDb.deleteAll(old uniqueSubtract new)
- mobileDb.insertAll(new uniqueSubtract old)
-
+ mobileDb.removeOldAndSaveNew(
+ oldItems = old uniqueSubtract new,
+ newItems = new uniqueSubtract old,
+ )
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, student))
}
)
suspend fun unregisterDevice(student: Student, semester: Semester, device: MobileDevice) {
- sdk.init(student)
- .switchSemester(semester)
+ wulkanowySdkFactory.create(student, semester)
.unregisterDevice(device.deviceId)
mobileDb.deleteAll(listOf(device))
}
suspend fun getToken(student: Student, semester: Semester): MobileDeviceToken {
- return sdk.init(student)
- .switchSemester(semester)
+ return wulkanowySdkFactory.create(student, semester)
.getToken()
.mapToMobileDeviceToken()
}
diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/NoteRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/NoteRepository.kt
index eeb1d53ef..9551e01eb 100644
--- a/app/src/main/java/io/github/wulkanowy/data/repositories/NoteRepository.kt
+++ b/app/src/main/java/io/github/wulkanowy/data/repositories/NoteRepository.kt
@@ -1,13 +1,16 @@
package io.github.wulkanowy.data.repositories
+import io.github.wulkanowy.data.WulkanowySdkFactory
import io.github.wulkanowy.data.db.dao.NoteDao
import io.github.wulkanowy.data.db.entities.Note
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.data.networkBoundResource
-import io.github.wulkanowy.sdk.Sdk
-import io.github.wulkanowy.utils.*
+import io.github.wulkanowy.utils.AutoRefreshHelper
+import io.github.wulkanowy.utils.getRefreshKey
+import io.github.wulkanowy.utils.toLocalDate
+import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.sync.Mutex
import javax.inject.Inject
@@ -16,7 +19,7 @@ import javax.inject.Singleton
@Singleton
class NoteRepository @Inject constructor(
private val noteDb: NoteDao,
- private val sdk: Sdk,
+ private val wulkanowySdkFactory: WulkanowySdkFactory,
private val refreshHelper: AutoRefreshHelper,
) {
@@ -40,20 +43,21 @@ class NoteRepository @Inject constructor(
},
query = { noteDb.loadAll(student.studentId) },
fetch = {
- sdk.init(student)
- .switchSemester(semester)
+ wulkanowySdkFactory.create(student, semester)
.getNotes()
.mapToEntities(semester)
},
saveFetchResult = { old, new ->
- noteDb.deleteAll(old uniqueSubtract new)
- noteDb.insertAll((new uniqueSubtract old).onEach {
+ val notesToAdd = (new uniqueSubtract old).onEach {
if (it.date >= student.registrationDate.toLocalDate()) it.apply {
isRead = false
if (notify) isNotified = false
}
- })
-
+ }
+ noteDb.removeOldAndSaveNew(
+ oldItems = old uniqueSubtract new,
+ newItems = notesToAdd,
+ )
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester))
}
)
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..2bb1538cb 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
@@ -10,6 +10,7 @@ import com.fredporciuncula.flow.preferences.Serializer
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R
import io.github.wulkanowy.data.enums.AppTheme
+import io.github.wulkanowy.data.enums.AttendanceCalculatorSortingMode
import io.github.wulkanowy.data.enums.GradeColorTheme
import io.github.wulkanowy.data.enums.GradeExpandMode
import io.github.wulkanowy.data.enums.GradeSortingMode
@@ -41,6 +42,27 @@ class PreferencesRepository @Inject constructor(
R.bool.pref_default_attendance_present
)
+ val targetAttendanceFlow: Flow
+ 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,
diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/RecipientRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/RecipientRepository.kt
index 79984ce6d..8233d932e 100644
--- a/app/src/main/java/io/github/wulkanowy/data/repositories/RecipientRepository.kt
+++ b/app/src/main/java/io/github/wulkanowy/data/repositories/RecipientRepository.kt
@@ -1,12 +1,15 @@
package io.github.wulkanowy.data.repositories
+import io.github.wulkanowy.data.WulkanowySdkFactory
import io.github.wulkanowy.data.db.dao.RecipientDao
-import io.github.wulkanowy.data.db.entities.*
+import io.github.wulkanowy.data.db.entities.Mailbox
+import io.github.wulkanowy.data.db.entities.MailboxType
+import io.github.wulkanowy.data.db.entities.Message
+import io.github.wulkanowy.data.db.entities.Recipient
+import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.mappers.mapToEntities
-import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.AutoRefreshHelper
import io.github.wulkanowy.utils.getRefreshKey
-import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.uniqueSubtract
import javax.inject.Inject
import javax.inject.Singleton
@@ -14,19 +17,22 @@ import javax.inject.Singleton
@Singleton
class RecipientRepository @Inject constructor(
private val recipientDb: RecipientDao,
- private val sdk: Sdk,
+ private val wulkanowySdkFactory: WulkanowySdkFactory,
private val refreshHelper: AutoRefreshHelper,
) {
private val cacheKey = "recipient"
suspend fun refreshRecipients(student: Student, mailbox: Mailbox, type: MailboxType) {
- val new = sdk.init(student).getRecipients(mailbox.globalKey)
+ val new = wulkanowySdkFactory.create(student)
+ .getRecipients(mailbox.globalKey)
.mapToEntities(mailbox.globalKey)
val old = recipientDb.loadAll(type, mailbox.globalKey)
- recipientDb.deleteAll(old uniqueSubtract new)
- recipientDb.insertAll(new uniqueSubtract old)
+ recipientDb.removeOldAndSaveNew(
+ oldItems = old uniqueSubtract new,
+ newItems = new uniqueSubtract old,
+ )
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, student))
}
@@ -54,7 +60,7 @@ class RecipientRepository @Inject constructor(
): List {
mailbox ?: return emptyList()
- return sdk.init(student)
+ return wulkanowySdkFactory.create(student)
.getMessageReplayDetails(message.messageGlobalKey)
.sender
.let(::listOf)
diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/RecoverRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/RecoverRepository.kt
index 5940f477b..b554bda0f 100644
--- a/app/src/main/java/io/github/wulkanowy/data/repositories/RecoverRepository.kt
+++ b/app/src/main/java/io/github/wulkanowy/data/repositories/RecoverRepository.kt
@@ -1,17 +1,23 @@
package io.github.wulkanowy.data.repositories
-import io.github.wulkanowy.sdk.Sdk
+import io.github.wulkanowy.data.WulkanowySdkFactory
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
-class RecoverRepository @Inject constructor(private val sdk: Sdk) {
+class RecoverRepository @Inject constructor(
+ private val wulkanowySdkFactory: WulkanowySdkFactory
+) {
- suspend fun getReCaptchaSiteKey(host: String, symbol: String): Pair {
- return sdk.getPasswordResetCaptchaCode(host, symbol)
- }
+ suspend fun getReCaptchaSiteKey(host: String, symbol: String): Pair =
+ wulkanowySdkFactory.create()
+ .getPasswordResetCaptchaCode(host, symbol)
suspend fun sendRecoverRequest(
- url: String, symbol: String, email: String, reCaptchaResponse: String
- ): String = sdk.sendPasswordResetRequest(url, symbol, email, reCaptchaResponse)
+ url: String,
+ symbol: String,
+ email: String,
+ reCaptchaResponse: String
+ ): String = wulkanowySdkFactory.create()
+ .sendPasswordResetRequest(url, symbol, email, reCaptchaResponse)
}
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 4c42d092f..6a04ce75f 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
@@ -1,14 +1,13 @@
package io.github.wulkanowy.data.repositories
+import io.github.wulkanowy.data.WulkanowySdkFactory
import io.github.wulkanowy.data.db.dao.SchoolAnnouncementDao
import io.github.wulkanowy.data.db.entities.SchoolAnnouncement
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.data.networkBoundResource
-import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.AutoRefreshHelper
import io.github.wulkanowy.utils.getRefreshKey
-import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.sync.Mutex
@@ -18,7 +17,7 @@ import javax.inject.Singleton
@Singleton
class SchoolAnnouncementRepository @Inject constructor(
private val schoolAnnouncementDb: SchoolAnnouncementDao,
- private val sdk: Sdk,
+ private val wulkanowySdkFactory: WulkanowySdkFactory,
private val refreshHelper: AutoRefreshHelper,
) {
@@ -41,17 +40,18 @@ class SchoolAnnouncementRepository @Inject constructor(
schoolAnnouncementDb.loadAll(student.userLoginId)
},
fetch = {
- sdk.init(student)
- .getDirectorInformation()
- .mapToEntities(student)
+ val sdk = wulkanowySdkFactory.create(student)
+ val lastAnnouncements = sdk.getLastAnnouncements().mapToEntities(student)
+ val directorInformation = sdk.getDirectorInformation().mapToEntities(student)
+ lastAnnouncements + directorInformation
},
saveFetchResult = { old, new ->
- val schoolAnnouncementsToSave = (new uniqueSubtract old).onEach {
- if (notify) it.isNotified = false
- }
-
- schoolAnnouncementDb.deleteAll(old uniqueSubtract new)
- schoolAnnouncementDb.insertAll(schoolAnnouncementsToSave)
+ schoolAnnouncementDb.removeOldAndSaveNew(
+ oldItems = old uniqueSubtract new,
+ newItems = (new uniqueSubtract old).onEach {
+ if (notify) it.isNotified = false
+ },
+ )
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, student))
}
)
diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolRepository.kt
index f757ef047..c48abb6f8 100644
--- a/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolRepository.kt
+++ b/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolRepository.kt
@@ -1,15 +1,13 @@
package io.github.wulkanowy.data.repositories
+import io.github.wulkanowy.data.WulkanowySdkFactory
import io.github.wulkanowy.data.db.dao.SchoolDao
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.mappers.mapToEntity
import io.github.wulkanowy.data.networkBoundResource
-import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.AutoRefreshHelper
import io.github.wulkanowy.utils.getRefreshKey
-import io.github.wulkanowy.utils.init
-import io.github.wulkanowy.utils.switchSemester
import kotlinx.coroutines.sync.Mutex
import javax.inject.Inject
import javax.inject.Singleton
@@ -17,7 +15,7 @@ import javax.inject.Singleton
@Singleton
class SchoolRepository @Inject constructor(
private val schoolDb: SchoolDao,
- private val sdk: Sdk,
+ private val wulkanowySdkFactory: WulkanowySdkFactory,
private val refreshHelper: AutoRefreshHelper,
) {
@@ -40,17 +38,16 @@ class SchoolRepository @Inject constructor(
},
query = { schoolDb.load(semester.studentId, semester.classId) },
fetch = {
- sdk.init(student)
- .switchSemester(semester)
+ wulkanowySdkFactory.create(student, semester)
.getSchool()
.mapToEntity(semester)
},
saveFetchResult = { old, new ->
if (old != null && new != old) {
- with(schoolDb) {
- deleteAll(listOf(old))
- insertAll(listOf(new))
- }
+ schoolDb.removeOldAndSaveNew(
+ oldItems = listOf(old),
+ newItems = listOf(new)
+ )
} else if (old == null) {
schoolDb.insertAll(listOf(new))
}
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 216a8c112..4a16d6f13 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,17 +1,15 @@
package io.github.wulkanowy.data.repositories
+import io.github.wulkanowy.data.WulkanowySdkFactory
import io.github.wulkanowy.data.api.SchoolsService
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.data.pojos.IntegrityRequest
import io.github.wulkanowy.data.pojos.LoginEvent
-import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.ui.modules.login.LoginData
import io.github.wulkanowy.utils.IntegrityHelper
import io.github.wulkanowy.utils.getCurrentOrLast
-import io.github.wulkanowy.utils.init
-import io.github.wulkanowy.utils.switchSemester
import kotlinx.coroutines.withTimeout
import timber.log.Timber
import java.util.UUID
@@ -23,7 +21,7 @@ import kotlin.time.Duration.Companion.seconds
class SchoolsRepository @Inject constructor(
private val integrityHelper: IntegrityHelper,
private val schoolsService: SchoolsService,
- private val sdk: Sdk,
+ private val wulkanowySdkFactory: WulkanowySdkFactory,
) {
suspend fun logSchoolLogin(loginData: LoginData, students: List) {
@@ -40,10 +38,9 @@ class SchoolsRepository @Inject constructor(
private suspend fun logLogin(loginData: LoginData, student: Student, semester: Semester) {
val requestId = UUID.randomUUID().toString()
val token = integrityHelper.getIntegrityToken(requestId) ?: return
+ val updatedStudent = student.copy(password = loginData.password)
- val schoolInfo = sdk
- .init(student.copy(password = loginData.password))
- .switchSemester(semester)
+ val schoolInfo = wulkanowySdkFactory.create(updatedStudent, semester)
.getSchool()
schoolsService.logLoginEvent(
diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/SemesterRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/SemesterRepository.kt
index dd44df70f..da21f59ac 100644
--- a/app/src/main/java/io/github/wulkanowy/data/repositories/SemesterRepository.kt
+++ b/app/src/main/java/io/github/wulkanowy/data/repositories/SemesterRepository.kt
@@ -1,11 +1,15 @@
package io.github.wulkanowy.data.repositories
+import io.github.wulkanowy.data.WulkanowySdkFactory
import io.github.wulkanowy.data.db.dao.SemesterDao
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.sdk.Sdk
-import io.github.wulkanowy.utils.*
+import io.github.wulkanowy.utils.DispatchersProvider
+import io.github.wulkanowy.utils.getCurrentOrLast
+import io.github.wulkanowy.utils.isCurrent
+import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.withContext
import timber.log.Timber
import javax.inject.Inject
@@ -14,8 +18,8 @@ import javax.inject.Singleton
@Singleton
class SemesterRepository @Inject constructor(
private val semesterDb: SemesterDao,
- private val sdk: Sdk,
- private val dispatchers: DispatchersProvider
+ private val wulkanowySdkFactory: WulkanowySdkFactory,
+ private val dispatchers: DispatchersProvider,
) {
suspend fun getSemesters(
@@ -45,6 +49,7 @@ class SemesterRepository @Inject constructor(
0 == it.diaryId && 0 == it.kindergartenDiaryId
} == true
}
+
else -> false
}
@@ -55,12 +60,17 @@ class SemesterRepository @Inject constructor(
}
private suspend fun refreshSemesters(student: Student) {
- val new = sdk.init(student).getSemesters().mapToEntities(student.studentId)
+ val new = wulkanowySdkFactory.create(student)
+ .getSemesters()
+ .mapToEntities(student.studentId)
+
if (new.isEmpty()) return Timber.i("Empty semester list!")
val old = semesterDb.loadAll(student.studentId, student.classId)
- semesterDb.deleteAll(old.uniqueSubtract(new))
- semesterDb.insertSemesters(new.uniqueSubtract(old))
+ semesterDb.removeOldAndSaveNew(
+ oldItems = old uniqueSubtract new,
+ newItems = new uniqueSubtract old,
+ )
}
suspend fun getCurrentSemester(student: Student, forceRefresh: Boolean = false) =
diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/StudentInfoRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/StudentInfoRepository.kt
index d6cd25c82..db4c0aebb 100644
--- a/app/src/main/java/io/github/wulkanowy/data/repositories/StudentInfoRepository.kt
+++ b/app/src/main/java/io/github/wulkanowy/data/repositories/StudentInfoRepository.kt
@@ -1,13 +1,11 @@
package io.github.wulkanowy.data.repositories
+import io.github.wulkanowy.data.WulkanowySdkFactory
import io.github.wulkanowy.data.db.dao.StudentInfoDao
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.mappers.mapToEntity
import io.github.wulkanowy.data.networkBoundResource
-import io.github.wulkanowy.sdk.Sdk
-import io.github.wulkanowy.utils.init
-import io.github.wulkanowy.utils.switchSemester
import kotlinx.coroutines.sync.Mutex
import javax.inject.Inject
import javax.inject.Singleton
@@ -15,7 +13,7 @@ import javax.inject.Singleton
@Singleton
class StudentInfoRepository @Inject constructor(
private val studentInfoDao: StudentInfoDao,
- private val sdk: Sdk
+ private val wulkanowySdkFactory: WulkanowySdkFactory,
) {
private val saveFetchResultMutex = Mutex()
@@ -30,16 +28,16 @@ class StudentInfoRepository @Inject constructor(
shouldFetch = { it == null || forceRefresh },
query = { studentInfoDao.loadStudentInfo(student.studentId) },
fetch = {
- sdk.init(student)
- .switchSemester(semester)
- .getStudentInfo().mapToEntity(semester)
+ wulkanowySdkFactory.create(student, semester)
+ .getStudentInfo()
+ .mapToEntity(semester)
},
saveFetchResult = { old, new ->
if (old != null && new != old) {
- with(studentInfoDao) {
- deleteAll(listOf(old))
- insertAll(listOf(new))
- }
+ studentInfoDao.removeOldAndSaveNew(
+ oldItems = listOf(old),
+ newItems = listOf(new),
+ )
} else if (old == null) {
studentInfoDao.insertAll(listOf(new))
}
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 e063840cb..9a5ecd538 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
@@ -1,6 +1,7 @@
package io.github.wulkanowy.data.repositories
import androidx.room.withTransaction
+import io.github.wulkanowy.data.WulkanowySdkFactory
import io.github.wulkanowy.data.db.AppDatabase
import io.github.wulkanowy.data.db.dao.SemesterDao
import io.github.wulkanowy.data.db.dao.StudentDao
@@ -14,9 +15,7 @@ import io.github.wulkanowy.data.mappers.mapToPojo
import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.DispatchersProvider
-import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.security.Scrambler
-import io.github.wulkanowy.utils.switchSemester
import kotlinx.coroutines.withContext
import javax.inject.Inject
import javax.inject.Singleton
@@ -26,7 +25,7 @@ class StudentRepository @Inject constructor(
private val dispatchers: DispatchersProvider,
private val studentDb: StudentDao,
private val semesterDb: SemesterDao,
- private val sdk: Sdk,
+ private val wulkanowySdkFactory: WulkanowySdkFactory,
private val appDatabase: AppDatabase,
private val scrambler: Scrambler,
) {
@@ -37,7 +36,7 @@ class StudentRepository @Inject constructor(
pin: String,
symbol: String,
token: String
- ): RegisterUser = sdk
+ ): RegisterUser = wulkanowySdkFactory.create()
.getStudentsFromHebe(token, pin, symbol, "")
.mapToPojo(null)
@@ -47,7 +46,7 @@ class StudentRepository @Inject constructor(
scrapperBaseUrl: String,
domainSuffix: String,
symbol: String
- ): RegisterUser = sdk
+ ): RegisterUser = wulkanowySdkFactory.create()
.getUserSubjectsFromScrapper(email, password, scrapperBaseUrl, domainSuffix, symbol)
.mapToPojo(password)
@@ -56,7 +55,7 @@ class StudentRepository @Inject constructor(
password: String,
scrapperBaseUrl: String,
symbol: String
- ): RegisterUser = sdk
+ ): RegisterUser = wulkanowySdkFactory.create()
.getStudentsHybrid(email, password, scrapperBaseUrl, "", symbol)
.mapToPojo(password)
@@ -149,13 +148,11 @@ class StudentRepository @Inject constructor(
.distinctBy { it.student.studentName }.size == 1
suspend fun authorizePermission(student: Student, semester: Semester, pesel: String) =
- sdk.init(student)
- .switchSemester(semester)
+ wulkanowySdkFactory.create(student, semester)
.authorizePermission(pesel)
suspend fun refreshStudentName(student: Student, semester: Semester) {
- val newCurrentApiStudent = sdk.init(student)
- .switchSemester(semester)
+ val newCurrentApiStudent = wulkanowySdkFactory.create(student, semester)
.getCurrentStudent() ?: return
val studentName = StudentName(
diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/SubjectRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/SubjectRepository.kt
index 98cb181af..573c7c149 100644
--- a/app/src/main/java/io/github/wulkanowy/data/repositories/SubjectRepository.kt
+++ b/app/src/main/java/io/github/wulkanowy/data/repositories/SubjectRepository.kt
@@ -1,15 +1,13 @@
package io.github.wulkanowy.data.repositories
+import io.github.wulkanowy.data.WulkanowySdkFactory
import io.github.wulkanowy.data.db.dao.SubjectDao
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.data.networkBoundResource
-import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.AutoRefreshHelper
import io.github.wulkanowy.utils.getRefreshKey
-import io.github.wulkanowy.utils.init
-import io.github.wulkanowy.utils.switchSemester
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.sync.Mutex
import javax.inject.Inject
@@ -18,7 +16,7 @@ import javax.inject.Singleton
@Singleton
class SubjectRepository @Inject constructor(
private val subjectDao: SubjectDao,
- private val sdk: Sdk,
+ private val wulkanowySdkFactory: WulkanowySdkFactory,
private val refreshHelper: AutoRefreshHelper,
) {
@@ -39,15 +37,15 @@ class SubjectRepository @Inject constructor(
},
query = { subjectDao.loadAll(semester.diaryId, semester.studentId) },
fetch = {
- sdk.init(student)
- .switchSemester(semester)
+ wulkanowySdkFactory.create(student, semester)
.getSubjects()
.mapToEntities(semester)
},
saveFetchResult = { old, new ->
- subjectDao.deleteAll(old uniqueSubtract new)
- subjectDao.insertAll(new uniqueSubtract old)
-
+ subjectDao.removeOldAndSaveNew(
+ oldItems = old uniqueSubtract new,
+ newItems = new uniqueSubtract old
+ )
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester))
}
)
diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/TeacherRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/TeacherRepository.kt
index 42698f922..a5a6e3f9c 100644
--- a/app/src/main/java/io/github/wulkanowy/data/repositories/TeacherRepository.kt
+++ b/app/src/main/java/io/github/wulkanowy/data/repositories/TeacherRepository.kt
@@ -1,15 +1,13 @@
package io.github.wulkanowy.data.repositories
+import io.github.wulkanowy.data.WulkanowySdkFactory
import io.github.wulkanowy.data.db.dao.TeacherDao
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.data.networkBoundResource
-import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.AutoRefreshHelper
import io.github.wulkanowy.utils.getRefreshKey
-import io.github.wulkanowy.utils.init
-import io.github.wulkanowy.utils.switchSemester
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.sync.Mutex
import javax.inject.Inject
@@ -18,7 +16,7 @@ import javax.inject.Singleton
@Singleton
class TeacherRepository @Inject constructor(
private val teacherDb: TeacherDao,
- private val sdk: Sdk,
+ private val wulkanowySdkFactory: WulkanowySdkFactory,
private val refreshHelper: AutoRefreshHelper,
) {
@@ -39,15 +37,15 @@ class TeacherRepository @Inject constructor(
},
query = { teacherDb.loadAll(semester.studentId, semester.classId) },
fetch = {
- sdk.init(student)
- .switchSemester(semester)
+ wulkanowySdkFactory.create(student, semester)
.getTeachers()
.mapToEntities(semester)
},
saveFetchResult = { old, new ->
- teacherDb.deleteAll(old uniqueSubtract new)
- teacherDb.insertAll(new uniqueSubtract old)
-
+ teacherDb.removeOldAndSaveNew(
+ oldItems = old uniqueSubtract new,
+ newItems = new uniqueSubtract old,
+ )
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester))
}
)
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 9305d3b31..335789991 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
@@ -1,15 +1,23 @@
package io.github.wulkanowy.data.repositories
+import io.github.wulkanowy.data.WulkanowySdkFactory
import io.github.wulkanowy.data.db.dao.TimetableAdditionalDao
import io.github.wulkanowy.data.db.dao.TimetableDao
import io.github.wulkanowy.data.db.dao.TimetableHeaderDao
-import io.github.wulkanowy.data.db.entities.*
+import io.github.wulkanowy.data.db.entities.Semester
+import io.github.wulkanowy.data.db.entities.Student
+import io.github.wulkanowy.data.db.entities.Timetable
+import io.github.wulkanowy.data.db.entities.TimetableAdditional
+import io.github.wulkanowy.data.db.entities.TimetableHeader
import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.data.networkBoundResource
import io.github.wulkanowy.data.pojos.TimetableFull
-import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.services.alarm.TimetableNotificationSchedulerHelper
-import io.github.wulkanowy.utils.*
+import io.github.wulkanowy.utils.AutoRefreshHelper
+import io.github.wulkanowy.utils.getRefreshKey
+import io.github.wulkanowy.utils.monday
+import io.github.wulkanowy.utils.sunday
+import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.sync.Mutex
@@ -23,7 +31,7 @@ class TimetableRepository @Inject constructor(
private val timetableDb: TimetableDao,
private val timetableAdditionalDb: TimetableAdditionalDao,
private val timetableHeaderDb: TimetableHeaderDao,
- private val sdk: Sdk,
+ private val wulkanowySdkFactory: WulkanowySdkFactory,
private val schedulerHelper: TimetableNotificationSchedulerHelper,
private val refreshHelper: AutoRefreshHelper,
) {
@@ -64,8 +72,7 @@ class TimetableRepository @Inject constructor(
},
query = { getFullTimetableFromDatabase(student, semester, start, end) },
fetch = {
- val timetableFull = sdk.init(student)
- .switchSemester(semester)
+ val timetableFull = wulkanowySdkFactory.create(student, semester)
.getTimetable(start.monday, end.sunday)
timetableFull.mapToEntities(semester)
@@ -121,12 +128,12 @@ class TimetableRepository @Inject constructor(
}
}
- fun getTimetableFromDatabase(
+ suspend fun getTimetableFromDatabase(
semester: Semester,
- from: LocalDate,
+ start: LocalDate,
end: LocalDate
- ): Flow> {
- return timetableDb.loadAll(semester.diaryId, semester.studentId, from, end)
+ ): List {
+ return timetableDb.load(semester.diaryId, semester.studentId, start, end)
}
suspend fun updateTimetable(timetable: List) {
@@ -144,8 +151,10 @@ class TimetableRepository @Inject constructor(
new.apply { if (notify) isNotified = false }
}
- timetableDb.deleteAll(lessonsToRemove)
- timetableDb.insertAll(lessonsToAdd)
+ timetableDb.removeOldAndSaveNew(
+ oldItems = lessonsToRemove,
+ newItems = lessonsToAdd,
+ )
schedulerHelper.cancelScheduled(lessonsToRemove, student)
schedulerHelper.scheduleNotifications(lessonsToAdd, student)
@@ -156,13 +165,17 @@ class TimetableRepository @Inject constructor(
new: List
) {
val oldFiltered = old.filter { !it.isAddedByUser }
- timetableAdditionalDb.deleteAll(oldFiltered uniqueSubtract new)
- timetableAdditionalDb.insertAll(new uniqueSubtract old)
+ timetableAdditionalDb.removeOldAndSaveNew(
+ oldItems = oldFiltered uniqueSubtract new,
+ newItems = new uniqueSubtract old,
+ )
}
private suspend fun refreshDayHeaders(old: List, new: List) {
- timetableHeaderDb.deleteAll(old uniqueSubtract new)
- timetableHeaderDb.insertAll(new uniqueSubtract old)
+ timetableHeaderDb.removeOldAndSaveNew(
+ oldItems = old uniqueSubtract new,
+ newItems = new uniqueSubtract old,
+ )
}
fun getLastRefreshTimestamp(semester: Semester, start: LocalDate, end: LocalDate): Instant {
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/timetable/IsStudentHasLessonsOnWeekendUseCase.kt b/app/src/main/java/io/github/wulkanowy/domain/timetable/IsStudentHasLessonsOnWeekendUseCase.kt
index efe928e2b..ffd005740 100644
--- a/app/src/main/java/io/github/wulkanowy/domain/timetable/IsStudentHasLessonsOnWeekendUseCase.kt
+++ b/app/src/main/java/io/github/wulkanowy/domain/timetable/IsStudentHasLessonsOnWeekendUseCase.kt
@@ -1,10 +1,7 @@
package io.github.wulkanowy.domain.timetable
-import io.github.wulkanowy.data.dataOrNull
import io.github.wulkanowy.data.db.entities.Semester
-import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.repositories.TimetableRepository
-import io.github.wulkanowy.data.toFirstResult
import io.github.wulkanowy.utils.monday
import io.github.wulkanowy.utils.sunday
import java.time.LocalDate
@@ -16,18 +13,14 @@ class IsStudentHasLessonsOnWeekendUseCase @Inject constructor(
) {
suspend operator fun invoke(
- student: Student,
semester: Semester,
currentDate: LocalDate = LocalDate.now(),
): Boolean {
- val lessons = timetableRepository.getTimetable(
- student = student,
+ val lessons = timetableRepository.getTimetableFromDatabase(
semester = semester,
start = currentDate.monday,
end = currentDate.sunday,
- forceRefresh = false,
- timetableType = TimetableRepository.TimetableType.NORMAL
- ).toFirstResult().dataOrNull?.lessons.orEmpty()
+ )
return isWeekendHasLessonsUseCase(lessons)
}
}
diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/works/TimetableWork.kt b/app/src/main/java/io/github/wulkanowy/services/sync/works/TimetableWork.kt
index ac9a8eb4c..2d10d925c 100644
--- a/app/src/main/java/io/github/wulkanowy/services/sync/works/TimetableWork.kt
+++ b/app/src/main/java/io/github/wulkanowy/services/sync/works/TimetableWork.kt
@@ -6,7 +6,6 @@ import io.github.wulkanowy.data.repositories.TimetableRepository
import io.github.wulkanowy.data.waitForResult
import io.github.wulkanowy.services.sync.notifications.ChangeTimetableNotification
import io.github.wulkanowy.utils.nextOrSameSchoolDay
-import kotlinx.coroutines.flow.first
import java.time.LocalDate.now
import javax.inject.Inject
@@ -31,10 +30,9 @@ class TimetableWork @Inject constructor(
timetableRepository.getTimetableFromDatabase(
semester = semester,
- from = startDate,
+ start = startDate,
end = endDate,
)
- .first()
.filterNot { it.isNotified }
.let {
if (it.isNotEmpty()) changeTimetableNotification.notify(it, student)
diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/BaseActivity.kt b/app/src/main/java/io/github/wulkanowy/ui/base/BaseActivity.kt
index 29996db7c..922c35365 100644
--- a/app/src/main/java/io/github/wulkanowy/ui/base/BaseActivity.kt
+++ b/app/src/main/java/io/github/wulkanowy/ui/base/BaseActivity.kt
@@ -1,6 +1,7 @@
package io.github.wulkanowy.ui.base
import android.app.ActivityManager
+import android.os.Build
import android.os.Bundle
import android.view.View
import android.widget.Toast
@@ -17,6 +18,8 @@ import io.github.wulkanowy.utils.FragmentLifecycleLogger
import io.github.wulkanowy.utils.getThemeAttrColor
import io.github.wulkanowy.utils.lifecycleAwareVariable
import io.github.wulkanowy.utils.openInternetBrowser
+import timber.log.Timber
+import java.time.Instant
import javax.inject.Inject
abstract class BaseActivity, VB : ViewBinding> :
@@ -36,16 +39,26 @@ abstract class BaseActivity, VB : ViewBinding> :
abstract var presenter: T
+ private var lastDialogOpenTime = mutableMapOf()
+
override fun onCreate(savedInstanceState: Bundle?) {
inject()
themeManager.applyActivityTheme(this)
super.onCreate(savedInstanceState)
supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleLogger, true)
+ applyCustomTaskDescription()
+ }
- @Suppress("DEPRECATION")
- setTaskDescription(
- ActivityManager.TaskDescription(null, null, getThemeAttrColor(R.attr.colorSurface))
- )
+ @Suppress("DEPRECATION")
+ private fun applyCustomTaskDescription() {
+ if (Build.VERSION.SDK_INT > Build.VERSION_CODES.R) return
+ try {
+ val newColor = getThemeAttrColor(R.attr.colorSurface)
+ val taskDescription = ActivityManager.TaskDescription(null, null, newColor)
+ setTaskDescription(taskDescription)
+ } catch (e: Exception) {
+ Timber.e(e)
+ }
}
override fun showError(text: String, error: Throwable) {
@@ -70,6 +83,8 @@ abstract class BaseActivity, VB : ViewBinding> :
}
override fun showExpiredCredentialsDialog() {
+ if (!shouldShowDialog(DIALOG_ERROR_BAD_CREDENTIALS)) return
+
MaterialAlertDialogBuilder(this)
.setTitle(R.string.main_expired_credentials_title)
.setMessage(R.string.main_expired_credentials_description)
@@ -83,6 +98,8 @@ abstract class BaseActivity, VB : ViewBinding> :
}
override fun showDecryptionFailedDialog() {
+ if (!shouldShowDialog(DIALOG_ERROR_DECRYPTION_FAILED)) return
+
MaterialAlertDialogBuilder(this)
.setTitle(R.string.main_session_expired)
.setMessage(R.string.main_session_relogin)
@@ -119,4 +136,21 @@ abstract class BaseActivity, VB : ViewBinding> :
protected open fun inject() {
throw UnsupportedOperationException()
}
+
+ private fun shouldShowDialog(name: String): Boolean {
+ val lastOpenTime = lastDialogOpenTime[name]
+ val now = Instant.now()
+
+ if (lastOpenTime != null && now.isBefore(lastOpenTime.plusSeconds(1))) {
+ Timber.i("Dialog $name was shown less than a second ago. Skip")
+ return false
+ }
+ lastDialogOpenTime[name] = Instant.now()
+ return true
+ }
+
+ companion object {
+ private const val DIALOG_ERROR_BAD_CREDENTIALS = "dialog_error_bad_credentials"
+ private const val DIALOG_ERROR_DECRYPTION_FAILED = "dialog_error_decryption_failed"
+ }
}
diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/ErrorHandler.kt b/app/src/main/java/io/github/wulkanowy/ui/base/ErrorHandler.kt
index e17c0c9ec..7109f1ffd 100644
--- a/app/src/main/java/io/github/wulkanowy/ui/base/ErrorHandler.kt
+++ b/app/src/main/java/io/github/wulkanowy/ui/base/ErrorHandler.kt
@@ -34,7 +34,7 @@ open class ErrorHandler @Inject constructor(@ApplicationContext protected val co
}
protected open fun proceed(error: Throwable) {
- showErrorMessage(context.resources.getErrorString(error), error)
+ showDefaultMessage(error)
when (error) {
is PasswordChangeRequiredException -> onPasswordChangeRequired(error.redirectUrl)
is ScramblerException -> onDecryptionFailed()
@@ -45,6 +45,10 @@ open class ErrorHandler @Inject constructor(@ApplicationContext protected val co
}
}
+ fun showDefaultMessage(error: Throwable) {
+ showErrorMessage(context.resources.getErrorString(error), error)
+ }
+
open fun clear() {
showErrorMessage = { _, _ -> }
onExpiredCredentials = {}
diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceAdapter.kt
index 4e9baac3a..f5689ec8d 100644
--- a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceAdapter.kt
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceAdapter.kt
@@ -1,5 +1,6 @@
package io.github.wulkanowy.ui.modules.attendance
+import android.content.res.ColorStateList
import android.graphics.Typeface
import android.view.LayoutInflater
import android.view.View
@@ -33,17 +34,17 @@ class AttendanceAdapter @Inject constructor() :
)
override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
+ val context = holder.binding.root.context
val item = items[position]
with(holder.binding) {
attendanceItemNumber.text = item.number.toString()
- attendanceItemSubject.text = item.subject.ifBlank {
- root.context.getString(R.string.all_no_data)
- }
+ attendanceItemSubject.text = item.subject
+ .ifBlank { context.getString(R.string.all_no_data) }
attendanceItemDescription.setText(item.descriptionRes)
attendanceItemDescription.setTextColor(
- root.context.getThemeAttrColor(
+ context.getThemeAttrColor(
when {
item.absence && !item.excused -> R.attr.colorAttendanceAbsence
item.lateness && !item.excused -> R.attr.colorAttendanceLateness
@@ -61,13 +62,15 @@ class AttendanceAdapter @Inject constructor() :
attendanceItemAlert.isVisible =
item.let { (it.absence && !it.excused) || (it.lateness && !it.excused) }
- attendanceItemAlert.setColorFilter(root.context.getThemeAttrColor(
- when{
- item.absence && !item.excused -> R.attr.colorAttendanceAbsence
- item.lateness && !item.excused -> R.attr.colorAttendanceLateness
- else -> android.R.attr.colorPrimary
- }
- ))
+ attendanceItemAlert.imageTintList = ColorStateList.valueOf(
+ context.getThemeAttrColor(
+ when {
+ item.absence && !item.excused -> R.attr.colorAttendanceAbsence
+ item.lateness && !item.excused -> R.attr.colorAttendanceLateness
+ else -> android.R.attr.colorPrimary
+ }
+ )
+ )
attendanceItemNumber.visibility = View.GONE
attendanceItemExcuseInfo.visibility = View.GONE
attendanceItemExcuseCheckbox.visibility = View.GONE
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 f66479daf..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,21 +1,37 @@
package io.github.wulkanowy.ui.modules.attendance
import android.annotation.SuppressLint
-import io.github.wulkanowy.data.*
+import io.github.wulkanowy.data.Resource
import io.github.wulkanowy.data.db.entities.Attendance
import io.github.wulkanowy.data.db.entities.Semester
-import io.github.wulkanowy.data.db.entities.Student
-import io.github.wulkanowy.data.db.entities.Timetable
+import io.github.wulkanowy.data.flatResourceFlow
+import io.github.wulkanowy.data.logResourceStatus
+import io.github.wulkanowy.data.mapResourceData
+import io.github.wulkanowy.data.onResourceData
+import io.github.wulkanowy.data.onResourceError
+import io.github.wulkanowy.data.onResourceIntermediate
+import io.github.wulkanowy.data.onResourceLoading
+import io.github.wulkanowy.data.onResourceNotLoading
+import io.github.wulkanowy.data.onResourceSuccess
import io.github.wulkanowy.data.repositories.AttendanceRepository
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.SemesterRepository
import io.github.wulkanowy.data.repositories.StudentRepository
-import io.github.wulkanowy.data.repositories.TimetableRepository
+import io.github.wulkanowy.data.resourceFlow
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
-import io.github.wulkanowy.utils.*
-import kotlinx.coroutines.flow.catch
-import kotlinx.coroutines.flow.flow
+import io.github.wulkanowy.utils.AnalyticsHelper
+import io.github.wulkanowy.utils.capitalise
+import io.github.wulkanowy.utils.getLastSchoolDayIfHoliday
+import io.github.wulkanowy.utils.isExcusableOrNotExcused
+import io.github.wulkanowy.utils.isHolidays
+import io.github.wulkanowy.utils.monday
+import io.github.wulkanowy.utils.nextSchoolDay
+import io.github.wulkanowy.utils.previousOrSameSchoolDay
+import io.github.wulkanowy.utils.previousSchoolDay
+import io.github.wulkanowy.utils.sunday
+import io.github.wulkanowy.utils.toFormattedString
+import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.onEach
import timber.log.Timber
import java.time.DayOfWeek
@@ -199,6 +215,11 @@ class AttendancePresenter @Inject constructor(
return true
}
+ fun onCalculatorSwitchSelected(): Boolean {
+ view?.openCalculatorView()
+ return true
+ }
+
private fun loadData(forceRefresh: Boolean = false) {
Timber.i("Loading attendance data started")
@@ -210,7 +231,7 @@ class AttendancePresenter @Inject constructor(
val semester = semesterRepository.getCurrentSemester(student)
- checkInitialAndCurrentDate(student, semester)
+ checkInitialAndCurrentDate(semester)
attendanceRepository.getAttendance(
student = student,
semester = semester,
@@ -266,15 +287,13 @@ class AttendancePresenter @Inject constructor(
.launch()
}
- private suspend fun checkInitialAndCurrentDate(student: Student, semester: Semester) {
+ private suspend fun checkInitialAndCurrentDate(semester: Semester) {
if (initialDate == null) {
- val lessons = attendanceRepository.getAttendance(
- student = student,
+ val lessons = attendanceRepository.getAttendanceFromDatabase(
semester = semester,
start = now().monday,
end = now().sunday,
- forceRefresh = false,
- ).toFirstResult().dataOrNull.orEmpty()
+ ).firstOrNull().orEmpty()
isWeekendHasLessons = isWeekendHasLessons(lessons)
initialDate = getInitialDate(semester)
}
@@ -316,6 +335,7 @@ class AttendancePresenter @Inject constructor(
showContent(false)
showExcuseButton(false)
}
+
is Resource.Success -> {
Timber.i("Excusing for absence result: Success")
analytics.logEvent("excuse_absence", "items" to attendanceToExcuseList.size)
@@ -328,6 +348,7 @@ class AttendancePresenter @Inject constructor(
}
loadData(forceRefresh = true)
}
+
is Resource.Error -> {
Timber.i("Excusing for absence result: An exception occurred")
errorHandler.dispatch(it.error)
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..2d5667015
--- /dev/null
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/calculator/AttendanceCalculatorFragment.kt
@@ -0,0 +1,105 @@
+package io.github.wulkanowy.ui.modules.attendance.calculator
+
+import android.os.Bundle
+import android.view.View
+import androidx.core.view.isVisible
+import androidx.recyclerview.widget.LinearLayoutManager
+import dagger.hilt.android.AndroidEntryPoint
+import io.github.wulkanowy.R
+import io.github.wulkanowy.data.pojos.AttendanceData
+import io.github.wulkanowy.databinding.FragmentAttendanceCalculatorBinding
+import io.github.wulkanowy.ui.base.BaseFragment
+import io.github.wulkanowy.ui.modules.main.MainView
+import io.github.wulkanowy.ui.widgets.DividerItemDecoration
+import io.github.wulkanowy.utils.getThemeAttrColor
+import javax.inject.Inject
+
+@AndroidEntryPoint
+class AttendanceCalculatorFragment :
+ BaseFragment(R.layout.fragment_attendance_calculator),
+ AttendanceCalculatorView, MainView.TitledView {
+
+ @Inject
+ lateinit var presenter: AttendanceCalculatorPresenter
+
+ @Inject
+ lateinit var attendanceCalculatorAdapter: AttendanceCalculatorAdapter
+
+ override val titleStringId get() = R.string.attendance_title
+
+ companion object {
+ fun newInstance() = AttendanceCalculatorFragment()
+ }
+
+ override val isViewEmpty get() = attendanceCalculatorAdapter.items.isEmpty()
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ binding = FragmentAttendanceCalculatorBinding.bind(view)
+ messageContainer = binding.attendanceCalculatorRecycler
+ presenter.onAttachView(this)
+ }
+
+ override fun initView() {
+ with(binding.attendanceCalculatorRecycler) {
+ layoutManager = LinearLayoutManager(context)
+ adapter = attendanceCalculatorAdapter
+ addItemDecoration(DividerItemDecoration(context))
+ }
+
+ with(binding) {
+ attendanceCalculatorSwipe.setOnRefreshListener(presenter::onSwipeRefresh)
+ attendanceCalculatorSwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary))
+ attendanceCalculatorSwipe.setProgressBackgroundColorSchemeColor(requireContext().getThemeAttrColor(R.attr.colorSwipeRefresh))
+ attendanceCalculatorErrorRetry.setOnClickListener { presenter.onRetry() }
+ attendanceCalculatorErrorDetails.setOnClickListener { presenter.onDetailsClick() }
+ }
+ }
+
+ override fun updateData(data: List) {
+ 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..d292e5650
--- /dev/null
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/calculator/AttendanceCalculatorPresenter.kt
@@ -0,0 +1,84 @@
+package io.github.wulkanowy.ui.modules.attendance.calculator
+
+import io.github.wulkanowy.data.*
+import io.github.wulkanowy.data.repositories.SemesterRepository
+import io.github.wulkanowy.data.repositories.StudentRepository
+import io.github.wulkanowy.domain.attendance.GetAttendanceCalculatorDataUseCase
+import io.github.wulkanowy.ui.base.BasePresenter
+import io.github.wulkanowy.ui.base.ErrorHandler
+import timber.log.Timber
+import javax.inject.Inject
+
+class AttendanceCalculatorPresenter @Inject constructor(
+ errorHandler: ErrorHandler,
+ studentRepository: StudentRepository,
+ private val semesterRepository: SemesterRepository,
+ private val getAttendanceCalculatorData: GetAttendanceCalculatorDataUseCase,
+) : BasePresenter(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)
+ }
+ }
+}
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..94e661212
--- /dev/null
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/calculator/AttendanceCalculatorView.kt
@@ -0,0 +1,29 @@
+package io.github.wulkanowy.ui.modules.attendance.calculator
+
+import io.github.wulkanowy.data.pojos.AttendanceData
+import io.github.wulkanowy.ui.base.BaseView
+
+interface AttendanceCalculatorView : BaseView {
+
+ val isViewEmpty: Boolean
+
+ fun initView()
+
+ fun showRefresh(show: Boolean)
+
+ fun showContent(show: Boolean)
+
+ fun showProgress(show: Boolean)
+
+ fun enableSwipe(enable: Boolean)
+
+ fun showEmpty(show: Boolean)
+
+ fun showErrorView(show: Boolean)
+
+ fun setErrorDetails(message: String)
+
+ fun updateData(data: List)
+
+ fun clearView()
+}
diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/auth/AuthDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/auth/AuthDialog.kt
index fa29df473..0f7c4234e 100644
--- a/app/src/main/java/io/github/wulkanowy/ui/modules/auth/AuthDialog.kt
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/auth/AuthDialog.kt
@@ -78,4 +78,9 @@ class AuthDialog : BaseDialogFragment(), AuthView {
override fun showDescriptionWithName(name: String) {
binding.authDescription.text = getString(R.string.auth_description, name).parseAsHtml()
}
+
+ override fun onDestroyView() {
+ presenter.onDetachView()
+ super.onDestroyView()
+ }
}
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 8f579712b..3c061f498 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
@@ -62,7 +62,11 @@ class AuthPresenter @Inject constructor(
}
isSuccess
}
- .onFailure { errorHandler.dispatch(it) }
+ .onFailure {
+ errorHandler.dispatch(it)
+ view?.showProgress(false)
+ view?.showContent(true)
+ }
.onSuccess {
if (it) {
view?.showSuccess(true)
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 ed8293a9f..ce2173d28 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
@@ -10,9 +10,10 @@ import android.webkit.WebViewClient
import androidx.core.os.bundleOf
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
+import io.github.wulkanowy.data.WulkanowySdkFactory
import io.github.wulkanowy.databinding.DialogCaptchaBinding
-import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.ui.base.BaseDialogFragment
+import io.github.wulkanowy.utils.WebkitCookieManagerProxy
import timber.log.Timber
import javax.inject.Inject
@@ -20,7 +21,10 @@ import javax.inject.Inject
class CaptchaDialog : BaseDialogFragment() {
@Inject
- lateinit var sdk: Sdk
+ lateinit var wulkanowySdkFactory: WulkanowySdkFactory
+
+ @Inject
+ lateinit var webkitCookieManagerProxy: WebkitCookieManagerProxy
private var webView: WebView? = null
@@ -55,7 +59,7 @@ class CaptchaDialog : BaseDialogFragment() {
webView = this
with(settings) {
javaScriptEnabled = true
- userAgentString = sdk.userAgent
+ userAgentString = wulkanowySdkFactory.create().userAgent
}
webViewClient = object : WebViewClient() {
@@ -80,6 +84,7 @@ class CaptchaDialog : BaseDialogFragment() {
}
override fun onDestroy() {
+ webkitCookieManagerProxy.webkitCookieManager?.flush()
webView?.destroy()
super.onDestroy()
}
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 1e6f1c198..3fec62562 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
@@ -304,6 +304,7 @@ class DashboardPresenter @Inject constructor(
forceRefresh = forceRefresh
)
}
+ .mapResourceData { it.map { messageWithAuthor -> messageWithAuthor.message } }
.onResourceError { errorHandler.dispatch(it) }
.takeIf { DashboardItem.Tile.MESSAGES in selectedTiles } ?: flowSuccess
@@ -438,7 +439,7 @@ class DashboardPresenter @Inject constructor(
private fun loadLessons(student: Student, forceRefresh: Boolean) {
flatResourceFlow {
val semester = semesterRepository.getCurrentSemester(student)
- val date = when (isStudentHasLessonsOnWeekendUseCase(student, semester)) {
+ val date = when (isStudentHasLessonsOnWeekendUseCase(semester)) {
true -> LocalDate.now()
else -> LocalDate.now().nextOrSameSchoolDay
}
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 e8a5fa254..8da59eaf4 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
@@ -159,7 +159,7 @@ class GradeAverageProvider @Inject constructor(
?.updateModifiers(student, config).orEmpty()
(updatedSecondSemesterGrades + updatedFirstSemesterGrades).calcAverage(
- config.isOptionalArithmeticAverage
+ isOptionalArithmeticAverage = config.isOptionalArithmeticAverage,
)
} else {
secondSemesterSubject.average
@@ -173,13 +173,21 @@ class GradeAverageProvider @Inject constructor(
config: AverageCalcParams,
): Double {
return if (!isAnyVulcanAverage || config.forceAverageCalc) {
- val divider = if (secondSemesterSubject.grades.any { it.weightValue > .0 }) 2 else 1
+ val isSecondSemesterHasWeightGrade = secondSemesterSubject.grades
+ .any { it.weightValue > .0 }
+ val isSecondSemesterHasArithmeticGrade = secondSemesterSubject.grades
+ .all { it.weightValue == .0 } && config.isOptionalArithmeticAverage
+ val isSecondSemesterHaveAverage =
+ isSecondSemesterHasWeightGrade || isSecondSemesterHasArithmeticGrade
+
+ val divider = if (isSecondSemesterHaveAverage) 2 else 1
val secondSemesterAverage = secondSemesterSubject.grades
.updateModifiers(student, config)
- .calcAverage(config.isOptionalArithmeticAverage)
+ .calcAverage(isOptionalArithmeticAverage = config.isOptionalArithmeticAverage)
val firstSemesterAverage = firstSemesterSubject?.grades
?.updateModifiers(student, config)
- ?.calcAverage(config.isOptionalArithmeticAverage) ?: secondSemesterAverage
+ ?.calcAverage(isOptionalArithmeticAverage = config.isOptionalArithmeticAverage)
+ ?: secondSemesterAverage
(secondSemesterAverage + firstSemesterAverage) / divider
} else {
@@ -225,7 +233,7 @@ class GradeAverageProvider @Inject constructor(
subject = summary.subject,
average = if (!isAnyAverage || params.forceAverageCalc) {
grades.updateModifiers(student, params)
- .calcAverage(params.isOptionalArithmeticAverage)
+ .calcAverage(isOptionalArithmeticAverage = params.isOptionalArithmeticAverage)
} else summary.average,
points = summary.pointsSum,
summary = summary,
@@ -286,8 +294,13 @@ class GradeAverageProvider @Inject constructor(
proposedPoints = "",
finalPoints = "",
pointsSum = "",
- average = if (calcAverage) details.updateModifiers(student, params)
- .calcAverage(params.isOptionalArithmeticAverage) else .0
+ average = when {
+ calcAverage -> details
+ .updateModifiers(student, params)
+ .calcAverage(isOptionalArithmeticAverage = params.isOptionalArithmeticAverage)
+
+ else -> .0
+ }
)
}
}
diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenter.kt
index 69e1d027d..39bc3f02d 100644
--- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenter.kt
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenter.kt
@@ -14,6 +14,7 @@ import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.resourceFlow
import io.github.wulkanowy.domain.adminmessage.GetAppropriateAdminMessageUseCase
+import io.github.wulkanowy.sdk.scrapper.login.InvalidSymbolException
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.modules.login.LoginData
import io.github.wulkanowy.ui.modules.login.LoginErrorHandler
@@ -204,6 +205,9 @@ class LoginFormPresenter @Inject constructor(
}
.onResourceError {
loginErrorHandler.dispatch(it)
+ if (it is InvalidSymbolException) {
+ loginErrorHandler.showDefaultMessage(it)
+ }
lastError = it
view?.showContact(true)
analytics.logEvent(
diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/mailboxchooser/MailboxChooserDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/mailboxchooser/MailboxChooserDialog.kt
index 8bd84f2bf..11d3c6c12 100644
--- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/mailboxchooser/MailboxChooserDialog.kt
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/mailboxchooser/MailboxChooserDialog.kt
@@ -47,7 +47,6 @@ class MailboxChooserDialog : BaseDialogFragment(),
}
- @Suppress("UNCHECKED_CAST")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
presenter.onAttachView(
diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewAdapter.kt
index d3c6b95c7..b83f7e232 100644
--- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewAdapter.kt
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewAdapter.kt
@@ -50,12 +50,15 @@ class MessagePreviewAdapter @Inject constructor() :
ViewType.MESSAGE.id -> MessageViewHolder(
ItemMessagePreviewBinding.inflate(inflater, parent, false)
)
+
ViewType.DIVIDER.id -> DividerViewHolder(
ItemMessageDividerBinding.inflate(inflater, parent, false)
)
+
ViewType.ATTACHMENT.id -> AttachmentViewHolder(
ItemMessageAttachmentBinding.inflate(inflater, parent, false)
)
+
else -> throw IllegalStateException()
}
}
@@ -66,6 +69,7 @@ class MessagePreviewAdapter @Inject constructor() :
holder,
requireNotNull(messageWithAttachment).message
)
+
is AttachmentViewHolder -> bindAttachment(
holder,
requireNotNull(messageWithAttachment).attachments[position - 2]
@@ -82,9 +86,11 @@ class MessagePreviewAdapter @Inject constructor() :
recipientCount > 1 -> {
context.getString(R.string.message_read_by, message.readBy, recipientCount)
}
+
message.readBy == 1 || (isReceived && !message.unread) -> {
context.getString(R.string.message_read, context.getString(R.string.all_yes))
}
+
else -> context.getString(R.string.message_read, context.getString(R.string.all_no))
}
diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewFragment.kt
index 3ed685cd7..8e7c72765 100644
--- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewFragment.kt
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewFragment.kt
@@ -44,18 +44,33 @@ class MessagePreviewFragment :
private var menuForwardButton: MenuItem? = null
+ private var menuRestoreButton: MenuItem? = null
+
private var menuDeleteButton: MenuItem? = null
+ private var menuDeleteForeverButton: MenuItem? = null
+
private var menuShareButton: MenuItem? = null
private var menuPrintButton: MenuItem? = null
+ private var menuMuteButton: MenuItem? = null
+
override val titleStringId: Int
get() = R.string.message_title
override val deleteMessageSuccessString: String
get() = getString(R.string.message_delete_success)
+ override val muteMessageSuccessString: String
+ get() = getString(R.string.message_mute_success)
+
+ override val unmuteMessageSuccessString: String
+ get() = getString(R.string.message_unmute_success)
+
+ override val restoreMessageSuccessString: String
+ get() = getString(R.string.message_restore_success)
+
override val messageNoSubjectString: String
get() = getString(R.string.message_no_subject)
@@ -67,10 +82,10 @@ class MessagePreviewFragment :
get() = getString(R.string.message_not_exists)
companion object {
- const val MESSAGE_ID_KEY = "message_id"
+ private const val MESSAGE_ARG_KEY = "message"
fun newInstance(message: Message) = MessagePreviewFragment().apply {
- arguments = bundleOf(MESSAGE_ID_KEY to message)
+ arguments = bundleOf(MESSAGE_ARG_KEY to message)
}
}
@@ -86,7 +101,7 @@ class MessagePreviewFragment :
messageContainer = binding.messagePreviewContainer
presenter.onAttachView(
view = this,
- message = (savedInstanceState ?: arguments)?.serializable(MESSAGE_ID_KEY),
+ message = requireArguments().serializable(MESSAGE_ARG_KEY),
)
}
@@ -103,9 +118,12 @@ class MessagePreviewFragment :
inflater.inflate(R.menu.action_menu_message_preview, menu)
menuReplyButton = menu.findItem(R.id.messagePreviewMenuReply)
menuForwardButton = menu.findItem(R.id.messagePreviewMenuForward)
+ menuRestoreButton = menu.findItem(R.id.messagePreviewMenuRestore)
menuDeleteButton = menu.findItem(R.id.messagePreviewMenuDelete)
+ menuDeleteForeverButton = menu.findItem(R.id.messagePreviewMenuDeleteForever)
menuShareButton = menu.findItem(R.id.messagePreviewMenuShare)
menuPrintButton = menu.findItem(R.id.messagePreviewMenuPrint)
+ menuMuteButton = menu.findItem(R.id.messagePreviewMenuMute)
presenter.onCreateOptionsMenu()
menu.findItem(R.id.mainMenuAccount).isVisible = false
@@ -115,9 +133,12 @@ class MessagePreviewFragment :
return when (item.itemId) {
R.id.messagePreviewMenuReply -> presenter.onReply()
R.id.messagePreviewMenuForward -> presenter.onForward()
+ R.id.messagePreviewMenuRestore -> presenter.onMessageRestore()
R.id.messagePreviewMenuDelete -> presenter.onMessageDelete()
+ R.id.messagePreviewMenuDeleteForever -> presenter.onMessageDelete()
R.id.messagePreviewMenuShare -> presenter.onShare()
R.id.messagePreviewMenuPrint -> presenter.onPrint()
+ R.id.messagePreviewMenuMute -> presenter.onMute()
else -> false
}
}
@@ -129,6 +150,11 @@ class MessagePreviewFragment :
}
}
+ override fun updateMuteToggleButton(isMuted: Boolean) {
+ menuMuteButton?.setTitle(if (isMuted) R.string.message_unmute else R.string.message_mute)
+
+ }
+
override fun showProgress(show: Boolean) {
binding.messagePreviewProgress.visibility = if (show) VISIBLE else GONE
}
@@ -137,20 +163,15 @@ class MessagePreviewFragment :
binding.messagePreviewRecycler.visibility = if (show) VISIBLE else GONE
}
- override fun showOptions(show: Boolean, isReplayable: Boolean) {
- menuReplyButton?.isVisible = isReplayable
+ override fun showOptions(show: Boolean, isReplayable: Boolean, isRestorable: Boolean) {
+ menuReplyButton?.isVisible = show && isReplayable
menuForwardButton?.isVisible = show
- menuDeleteButton?.isVisible = show
+ menuRestoreButton?.isVisible = show && isRestorable
+ menuDeleteButton?.isVisible = show && !isRestorable
+ menuDeleteForeverButton?.isVisible = show && isRestorable
menuShareButton?.isVisible = show
menuPrintButton?.isVisible = show
- }
-
- override fun setDeletedOptionsLabels() {
- menuDeleteButton?.setTitle(R.string.message_delete_forever)
- }
-
- override fun setNotDeletedOptionsLabels() {
- menuDeleteButton?.setTitle(R.string.message_move_to_trash)
+ menuMuteButton?.isVisible = show && isReplayable
}
override fun showErrorView(show: Boolean) {
@@ -212,11 +233,6 @@ class MessagePreviewFragment :
(activity as MainActivity).popView()
}
- override fun onSaveInstanceState(outState: Bundle) {
- outState.putSerializable(MESSAGE_ID_KEY, presenter.message)
- super.onSaveInstanceState(outState)
- }
-
override fun onDestroyView() {
presenter.onDetachView()
super.onDestroyView()
diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewPresenter.kt
index cd7b72843..3b3b2b420 100644
--- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewPresenter.kt
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewPresenter.kt
@@ -3,10 +3,15 @@ package io.github.wulkanowy.ui.modules.message.preview
import android.annotation.SuppressLint
import androidx.core.text.parseAsHtml
import io.github.wulkanowy.R
-import io.github.wulkanowy.data.*
import io.github.wulkanowy.data.db.entities.Message
-import io.github.wulkanowy.data.db.entities.MessageAttachment
+import io.github.wulkanowy.data.db.entities.MessageWithAttachment
import io.github.wulkanowy.data.enums.MessageFolder
+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.MessageRepository
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.StudentRepository
@@ -14,9 +19,11 @@ import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.utils.AnalyticsHelper
import io.github.wulkanowy.utils.toFormattedString
+import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
+import kotlin.time.Duration.Companion.seconds
class MessagePreviewPresenter @Inject constructor(
errorHandler: ErrorHandler,
@@ -26,20 +33,17 @@ class MessagePreviewPresenter @Inject constructor(
private val analytics: AnalyticsHelper
) : BasePresenter(errorHandler, studentRepository) {
- var message: Message? = null
-
- var attachments: List? = null
+ private var messageWithAttachments: MessageWithAttachment? = null
private lateinit var lastError: Throwable
private var retryCallback: () -> Unit = {}
- fun onAttachView(view: MessagePreviewView, message: Message?) {
+ fun onAttachView(view: MessagePreviewView, message: Message) {
super.onAttachView(view)
view.initView()
errorHandler.showErrorMessage = ::showErrorViewOnError
- this.message = message
- loadData(requireNotNull(message))
+ loadData(message)
}
private fun onMessageLoadRetry(message: Message) {
@@ -66,25 +70,24 @@ class MessagePreviewPresenter @Inject constructor(
.logResourceStatus("message ${messageToLoad.messageId} preview")
.onResourceData {
if (it != null) {
- message = it.message
- attachments = it.attachments
+ messageWithAttachments = it
view?.apply {
setMessageWithAttachment(it)
showContent(true)
initOptions()
-
+ updateMuteToggleButton(isMuted = it.mutedMessageSender != null)
if (preferencesRepository.isIncognitoMode && it.message.unread) {
showMessage(R.string.message_incognito_description)
}
}
} else {
+ delay(1.seconds)
view?.run {
showMessage(messageNotExists)
popView()
}
}
- }
- .onResourceSuccess {
+ }.onResourceSuccess {
if (it != null) {
analytics.logEvent(
"load_item",
@@ -92,31 +95,28 @@ class MessagePreviewPresenter @Inject constructor(
"length" to it.message.content.length
)
}
- }
- .onResourceNotLoading { view?.showProgress(false) }
- .onResourceError {
+ }.onResourceNotLoading { view?.showProgress(false) }.onResourceError {
retryCallback = { onMessageLoadRetry(messageToLoad) }
errorHandler.dispatch(it)
- }
- .launch()
+ }.launch()
}
fun onReply(): Boolean {
- return if (message != null) {
- view?.openMessageReply(message)
+ return if (messageWithAttachments?.message != null) {
+ view?.openMessageReply(messageWithAttachments?.message)
true
} else false
}
fun onForward(): Boolean {
- return if (message != null) {
- view?.openMessageForward(message)
+ return if (messageWithAttachments?.message != null) {
+ view?.openMessageForward(messageWithAttachments?.message)
true
} else false
}
fun onShare(): Boolean {
- val message = message ?: return false
+ val message = messageWithAttachments?.message ?: return false
val subject = message.subject.ifBlank { view?.messageNoSubjectString.orEmpty() }
val text = buildString {
@@ -129,13 +129,15 @@ class MessagePreviewPresenter @Inject constructor(
appendLine(message.content.parseAsHtml())
- if (!attachments.isNullOrEmpty()) {
+ if (!messageWithAttachments?.attachments.isNullOrEmpty()) {
appendLine()
appendLine("Załączniki:")
- append(attachments.orEmpty().joinToString(separator = "\n") { attachment ->
- "${attachment.filename}: ${attachment.url}"
- })
+ append(
+ messageWithAttachments?.attachments.orEmpty()
+ .joinToString(separator = "\n") { attachment ->
+ "${attachment.filename}: ${attachment.url}"
+ })
}
}
@@ -148,7 +150,7 @@ class MessagePreviewPresenter @Inject constructor(
@SuppressLint("NewApi")
fun onPrint(): Boolean {
- val message = message ?: return false
+ val message = messageWithAttachments?.message ?: return false
val subject = message.subject.ifBlank { view?.messageNoSubjectString.orEmpty() }
val dateString = message.date.toFormattedString("yyyy-MM-dd HH:mm:ss")
@@ -159,8 +161,7 @@ class MessagePreviewPresenter @Inject constructor(
append("Od
${message.sender}
")
append("DO
${message.recipients}")
}
- val messageContent = "${message.content}
"
- .replace(Regex("[\\n\\r]{2,}"), "")
+ val messageContent = "
${message.content}
".replace(Regex("[\\n\\r]{2,}"), "")
.replace(Regex("[\\n\\r]"), "
")
val jobName = buildString {
@@ -171,9 +172,7 @@ class MessagePreviewPresenter @Inject constructor(
}
view?.apply {
- val html = printHTML
- .replace("%SUBJECT%", subject)
- .replace("%CONTENT%", messageContent)
+ val html = printHTML.replace("%SUBJECT%", subject).replace("%CONTENT%", messageContent)
.replace("%INFO%", infoContent)
printDocument(html, jobName)
}
@@ -181,34 +180,69 @@ class MessagePreviewPresenter @Inject constructor(
return true
}
- private fun deleteMessage() {
- message ?: return
+ private fun restoreMessage() {
+ val message = messageWithAttachments?.message ?: return
view?.run {
showContent(false)
showProgress(true)
- showOptions(show = false, isReplayable = false)
+ showOptions(
+ show = false,
+ isReplayable = false,
+ isRestorable = false,
+ )
showErrorView(false)
}
-
- Timber.i("Delete message ${message?.messageGlobalKey}")
-
+ Timber.i("Restore message ${message.messageGlobalKey}")
presenterScope.launch {
runCatching {
val student = studentRepository.getCurrentStudent(decryptPass = true)
val mailbox = messageRepository.getMailboxByStudent(student)
- messageRepository.deleteMessage(student, mailbox, message!!)
+ messageRepository.restoreMessages(student, mailbox, listOfNotNull(message))
}
.onFailure {
- retryCallback = { onMessageDelete() }
+ retryCallback = { onMessageRestore() }
errorHandler.dispatch(it)
}
.onSuccess {
view?.run {
- showMessage(deleteMessageSuccessString)
+ showMessage(restoreMessageSuccessString)
popView()
}
}
+ view?.showProgress(false)
+ }
+ }
+
+ private fun deleteMessage() {
+ messageWithAttachments?.message ?: return
+
+ view?.run {
+ showContent(false)
+ showProgress(true)
+ showOptions(
+ show = false,
+ isReplayable = false,
+ isRestorable = false,
+ )
+ showErrorView(false)
+ }
+
+ Timber.i("Delete message ${messageWithAttachments?.message?.messageGlobalKey}")
+
+ presenterScope.launch {
+ runCatching {
+ val student = studentRepository.getCurrentStudent(decryptPass = true)
+ messageRepository.deleteMessage(student, messageWithAttachments?.message!!)
+ }.onFailure {
+ retryCallback = { onMessageDelete() }
+ errorHandler.dispatch(it)
+ }.onSuccess {
+ view?.run {
+ showMessage(deleteMessageSuccessString)
+ popView()
+ }
+ }
view?.showProgress(false)
}
@@ -224,6 +258,11 @@ class MessagePreviewPresenter @Inject constructor(
}
}
+ fun onMessageRestore(): Boolean {
+ restoreMessage()
+ return true
+ }
+
fun onMessageDelete(): Boolean {
deleteMessage()
return true
@@ -232,20 +271,39 @@ class MessagePreviewPresenter @Inject constructor(
private fun initOptions() {
view?.apply {
showOptions(
- show = message != null,
- isReplayable = message?.folderId != MessageFolder.SENT.id,
+ show = messageWithAttachments?.message != null,
+ isReplayable = messageWithAttachments?.message?.folderId == MessageFolder.RECEIVED.id,
+ isRestorable = messageWithAttachments?.message?.folderId == MessageFolder.TRASHED.id,
)
- message?.let {
- when (it.folderId == MessageFolder.TRASHED.id) {
- true -> setDeletedOptionsLabels()
- false -> setNotDeletedOptionsLabels()
- }
- }
-
}
}
fun onCreateOptionsMenu() {
initOptions()
}
+
+ fun onMute(): Boolean {
+ val message = messageWithAttachments?.message ?: return false
+ val isMuted = messageWithAttachments?.mutedMessageSender != null
+
+ presenterScope.launch {
+ runCatching {
+ when (isMuted) {
+ true -> {
+ messageRepository.unmuteMessage(message.correspondents)
+ view?.run { showMessage(unmuteMessageSuccessString) }
+ }
+
+ false -> {
+ messageRepository.muteMessage(message.correspondents)
+ view?.run { showMessage(muteMessageSuccessString) }
+ }
+ }
+ }.onFailure {
+ errorHandler.dispatch(it)
+ }
+ }
+ view?.updateMuteToggleButton(isMuted)
+ return true
+ }
}
diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewView.kt
index 7f5f140b2..ee0b6ce0a 100644
--- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewView.kt
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewView.kt
@@ -9,6 +9,12 @@ interface MessagePreviewView : BaseView {
val deleteMessageSuccessString: String
+ val muteMessageSuccessString: String
+
+ val unmuteMessageSuccessString: String
+
+ val restoreMessageSuccessString: String
+
val messageNoSubjectString: String
val printHTML: String
@@ -19,6 +25,8 @@ interface MessagePreviewView : BaseView {
fun setMessageWithAttachment(item: MessageWithAttachment)
+ fun updateMuteToggleButton(isMuted: Boolean)
+
fun showProgress(show: Boolean)
fun showContent(show: Boolean)
@@ -29,11 +37,7 @@ interface MessagePreviewView : BaseView {
fun setErrorRetryCallback(callback: () -> Unit)
- fun showOptions(show: Boolean, isReplayable: Boolean)
-
- fun setDeletedOptionsLabels()
-
- fun setNotDeletedOptionsLabels()
+ fun showOptions(show: Boolean, isReplayable: Boolean, isRestorable: Boolean)
fun openMessageReply(message: Message?)
diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/SendMessagePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/SendMessagePresenter.kt
index e776e9941..6155baea3 100644
--- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/SendMessagePresenter.kt
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/SendMessagePresenter.kt
@@ -203,7 +203,7 @@ class SendMessagePresenter @Inject constructor(
subject = subject,
content = content,
recipients = recipients,
- mailboxId = mailbox.globalKey,
+ mailbox = mailbox,
)
}.logResourceStatus("sending message").onEach {
when (it) {
diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabAdapter.kt
index 9792c7085..fadc77e6d 100644
--- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabAdapter.kt
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabAdapter.kt
@@ -18,8 +18,7 @@ import io.github.wulkanowy.utils.getThemeAttrColor
import io.github.wulkanowy.utils.toFormattedString
import javax.inject.Inject
-class MessageTabAdapter @Inject constructor() :
- RecyclerView.Adapter() {
+class MessageTabAdapter @Inject constructor() : RecyclerView.Adapter() {
lateinit var onItemClickListener: (MessageTabDataItem.MessageItem, position: Int) -> Unit
@@ -52,10 +51,11 @@ class MessageTabAdapter @Inject constructor() :
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context)
- return when (MessageItemViewType.values()[viewType]) {
+ return when (MessageItemViewType.entries[viewType]) {
MessageItemViewType.FILTERS -> HeaderViewHolder(
ItemMessageChipsBinding.inflate(inflater, parent, false)
)
+
MessageItemViewType.MESSAGE -> ItemViewHolder(
ItemMessageBinding.inflate(inflater, parent, false)
)
@@ -137,7 +137,12 @@ class MessageTabAdapter @Inject constructor() :
ImageViewCompat.setImageTintList(this, ColorStateList.valueOf(currentTextColor))
isVisible = message.hasAttachments
}
- messageItemUnreadIndicator.isVisible = message.unread
+ messageItemUnreadIndicator.isVisible = message.unread || item.isMuted
+
+ when (item.isMuted) {
+ true -> messageItemUnreadIndicator.setImageResource(R.drawable.ic_notifications_off)
+ else -> messageItemUnreadIndicator.setImageResource(R.drawable.ic_circle_notification)
+ }
root.setOnClickListener {
holder.bindingAdapterPosition.let {
@@ -165,8 +170,7 @@ class MessageTabAdapter @Inject constructor() :
RecyclerView.ViewHolder(binding.root)
private class MessageTabDiffUtil(
- private val old: List,
- private val new: List
+ private val old: List, private val new: List
) : DiffUtil.Callback() {
override fun getOldListSize(): Int = old.size
diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabDataItem.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabDataItem.kt
index c0bd4170e..ef640e040 100644
--- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabDataItem.kt
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabDataItem.kt
@@ -6,6 +6,7 @@ sealed class MessageTabDataItem(val viewType: MessageItemViewType) {
data class MessageItem(
val message: Message,
+ val isMuted: Boolean,
val isSelected: Boolean,
val isActionMode: Boolean
) : MessageTabDataItem(MessageItemViewType.MESSAGE)
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 4364e8681..12f9d3234 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
@@ -5,7 +5,9 @@ import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
-import android.view.View.*
+import android.view.View.GONE
+import android.view.View.INVISIBLE
+import android.view.View.VISIBLE
import android.widget.CompoundButton
import androidx.annotation.StringRes
import androidx.appcompat.view.ActionMode
@@ -64,10 +66,12 @@ class MessageTabFragment : BaseFragment(R.layout.frag
}
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
- if (presenter.folder == MessageFolder.TRASHED) {
- val menuItem = menu.findItem(R.id.messageTabContextMenuDelete)
- menuItem.setTitle(R.string.message_delete_forever)
- }
+ val isTrashFolder = presenter.folder == MessageFolder.TRASHED
+
+ menu.findItem(R.id.messageTabContextMenuDelete).setVisible(!isTrashFolder)
+ menu.findItem(R.id.messageTabContextMenuDeleteForever).setVisible(isTrashFolder)
+ menu.findItem(R.id.messageTabContextMenuRestore).setVisible(isTrashFolder)
+
return presenter.onPrepareActionMode()
}
@@ -79,6 +83,8 @@ class MessageTabFragment : BaseFragment(R.layout.frag
override fun onActionItemClicked(mode: ActionMode, menu: MenuItem): Boolean {
when (menu.itemId) {
R.id.messageTabContextMenuDelete -> presenter.onActionModeSelectDelete()
+ R.id.messageTabContextMenuRestore -> presenter.onActionModeSelectRestore()
+ R.id.messageTabContextMenuDeleteForever -> presenter.onActionModeSelectDelete()
R.id.messageTabContextMenuSelectAll -> presenter.onActionModeSelectCheckAll()
}
return true
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 90f93b145..cda0b32bd 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
@@ -4,6 +4,7 @@ import io.github.wulkanowy.R
import io.github.wulkanowy.data.*
import io.github.wulkanowy.data.db.entities.Mailbox
import io.github.wulkanowy.data.db.entities.Message
+import io.github.wulkanowy.data.db.entities.MessageWithMutedAuthor
import io.github.wulkanowy.data.enums.MessageFolder
import io.github.wulkanowy.data.repositories.MessageRepository
import io.github.wulkanowy.data.repositories.StudentRepository
@@ -39,7 +40,7 @@ class MessageTabPresenter @Inject constructor(
private var mailboxes: List = emptyList()
private var selectedMailbox: Mailbox? = null
- private var messages = emptyList()
+ private var messages = emptyList()
private val searchChannel = Channel()
@@ -120,8 +121,27 @@ class MessageTabPresenter @Inject constructor(
return true
}
+ fun onActionModeSelectRestore() {
+ Timber.i("Restore ${messagesToDelete.size} messages")
+ val messageList = messagesToDelete.toList()
+
+ presenterScope.launch {
+ view?.run {
+ showProgress(true)
+ showContent(false)
+ showActionMode(false)
+ }
+ runCatching {
+ val student = studentRepository.getCurrentStudent(true)
+ messageRepository.restoreMessages(student, selectedMailbox, messageList)
+ }
+ .onFailure(errorHandler::dispatch)
+ .onSuccess { view?.showMessage(R.string.message_messages_restored) }
+ }
+ }
+
fun onActionModeSelectDelete() {
- Timber.i("Delete ${messagesToDelete.size} messages)")
+ Timber.i("Delete ${messagesToDelete.size} messages")
val messageList = messagesToDelete.toList()
presenterScope.launch {
@@ -133,7 +153,7 @@ class MessageTabPresenter @Inject constructor(
runCatching {
val student = studentRepository.getCurrentStudent(true)
- messageRepository.deleteMessages(student, selectedMailbox, messageList)
+ messageRepository.deleteMessages(student, messageList)
}
.onFailure(errorHandler::dispatch)
.onSuccess { view?.showMessage(R.string.message_messages_deleted) }
@@ -141,7 +161,7 @@ class MessageTabPresenter @Inject constructor(
}
fun onActionModeSelectCheckAll() {
- val messagesToSelect = getFilteredData()
+ val messagesToSelect = getFilteredData().map { it.message }
val isAllSelected = messagesToDelete.containsAll(messagesToSelect)
if (isAllSelected) {
@@ -188,7 +208,7 @@ class MessageTabPresenter @Inject constructor(
view?.showActionMode(false)
}
- val filteredData = getFilteredData()
+ val filteredData = getFilteredData().map { it.message }
view?.run {
updateActionModeTitle(messagesToDelete.size)
@@ -320,25 +340,31 @@ class MessageTabPresenter @Inject constructor(
}
}
- private fun getFilteredData(): List {
+ private fun getFilteredData(): List {
if (lastSearchQuery.trim().isEmpty()) {
- val sortedMessages = messages.sortedByDescending { it.date }
+ val sortedMessages = messages.sortedByDescending { it.message.date }
return when {
- (onlyUnread == true) && onlyWithAttachments -> sortedMessages.filter { it.unread == onlyUnread && it.hasAttachments == onlyWithAttachments }
- (onlyUnread == true) -> sortedMessages.filter { it.unread == onlyUnread }
- onlyWithAttachments -> sortedMessages.filter { it.hasAttachments == onlyWithAttachments }
+ (onlyUnread == true) && onlyWithAttachments -> sortedMessages.filter {
+ it.message.unread == onlyUnread && it.message.hasAttachments == onlyWithAttachments
+ }
+
+ (onlyUnread == true) -> sortedMessages.filter { it.message.unread == onlyUnread }
+ onlyWithAttachments -> sortedMessages.filter { it.message.hasAttachments == onlyWithAttachments }
else -> sortedMessages
}
} else {
val sortedMessages = messages
- .map { it to calculateMatchRatio(it, lastSearchQuery) }
- .sortedWith(compareBy> { -it.second }.thenByDescending { it.first.date })
+ .map { it to calculateMatchRatio(it.message, lastSearchQuery) }
+ .sortedWith(compareBy> { -it.second }.thenByDescending { it.first.message.date })
.filter { it.second > 6000 }
.map { it.first }
return when {
- (onlyUnread == true) && onlyWithAttachments -> sortedMessages.filter { it.unread == onlyUnread && it.hasAttachments == onlyWithAttachments }
- (onlyUnread == true) -> sortedMessages.filter { it.unread == onlyUnread }
- onlyWithAttachments -> sortedMessages.filter { it.hasAttachments == onlyWithAttachments }
+ (onlyUnread == true) && onlyWithAttachments -> sortedMessages.filter {
+ it.message.unread == onlyUnread && it.message.hasAttachments == onlyWithAttachments
+ }
+
+ (onlyUnread == true) -> sortedMessages.filter { it.message.unread == onlyUnread }
+ onlyWithAttachments -> sortedMessages.filter { it.message.hasAttachments == onlyWithAttachments }
else -> sortedMessages
}
}
@@ -367,8 +393,9 @@ class MessageTabPresenter @Inject constructor(
addAll(data.map { message ->
MessageTabDataItem.MessageItem(
- message = message,
- isSelected = messagesToDelete.any { it.messageGlobalKey == message.messageGlobalKey },
+ message = message.message,
+ isMuted = message.mutedMessageSender != null,
+ isSelected = messagesToDelete.any { it.messageGlobalKey == message.message.messageGlobalKey },
isActionMode = isActionMode
)
})
diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolannouncement/SchoolAnnouncementAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolannouncement/SchoolAnnouncementAdapter.kt
index 46999599b..731488a9c 100644
--- a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolannouncement/SchoolAnnouncementAdapter.kt
+++ b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolannouncement/SchoolAnnouncementAdapter.kt
@@ -2,6 +2,7 @@ package io.github.wulkanowy.ui.modules.schoolannouncement
import android.view.LayoutInflater
import android.view.ViewGroup
+import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import io.github.wulkanowy.data.db.entities.SchoolAnnouncement
import io.github.wulkanowy.databinding.ItemSchoolAnnouncementBinding
@@ -29,6 +30,10 @@ class SchoolAnnouncementAdapter @Inject constructor() :
schoolAnnouncementItemDate.text = item.date.toFormattedString()
schoolAnnouncementItemType.text = item.subject
schoolAnnouncementItemContent.text = item.content.parseUonetHtml()
+ with(schoolAnnouncementItemAuthor) {
+ text = item.author
+ isVisible = !item.author.isNullOrBlank()
+ }
root.setOnClickListener { onItemClickListener(item) }
}
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..ba234aae2 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
@@ -4,6 +4,7 @@ import android.content.SharedPreferences
import android.os.Bundle
import android.view.View
import androidx.preference.PreferenceFragmentCompat
+import androidx.preference.SeekBarPreference
import com.yariksoffice.lingver.Lingver
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
@@ -36,6 +37,15 @@ class AppearanceFragment : PreferenceFragmentCompat(),
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/TimetablePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetablePresenter.kt
index 7e8c876ef..e83f25176 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
@@ -3,7 +3,6 @@ package io.github.wulkanowy.ui.modules.timetable
import android.os.Handler
import android.os.Looper
import io.github.wulkanowy.data.db.entities.Semester
-import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.Timetable
import io.github.wulkanowy.data.enums.TimetableGapsMode.BETWEEN_AND_BEFORE_LESSONS
import io.github.wulkanowy.data.enums.TimetableGapsMode.NO_GAPS
@@ -150,7 +149,7 @@ class TimetablePresenter @Inject constructor(
val student = studentRepository.getCurrentStudent()
val semester = semesterRepository.getCurrentSemester(student)
- checkInitialAndCurrentDate(student, semester)
+ checkInitialAndCurrentDate(semester)
timetableRepository.getTimetable(
student = student,
semester = semester,
@@ -194,9 +193,9 @@ class TimetablePresenter @Inject constructor(
.launch()
}
- private suspend fun checkInitialAndCurrentDate(student: Student, semester: Semester) {
+ private suspend fun checkInitialAndCurrentDate(semester: Semester) {
if (initialDate == null) {
- isWeekendHasLessons = isStudentHasLessonsOnWeekendUseCase(student, semester)
+ isWeekendHasLessons = isStudentHasLessonsOnWeekendUseCase(semester)
initialDate = getInitialDate(semester)
}
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/BundleExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/BundleExtension.kt
index d3c9f8006..b1742b4fa 100644
--- a/app/src/main/java/io/github/wulkanowy/utils/BundleExtension.kt
+++ b/app/src/main/java/io/github/wulkanowy/utils/BundleExtension.kt
@@ -4,30 +4,31 @@ import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.os.Parcelable
+import androidx.core.os.BundleCompat
import java.io.Serializable
+// Even though API was introduced in 33, we use 34 as 33 is bugged in some scenarios.
+
inline fun Bundle.serializable(key: String): T = when {
- Build.VERSION.SDK_INT >= 33 -> getSerializable(key, T::class.java)!!
+ Build.VERSION.SDK_INT >= 34 -> getSerializable(key, T::class.java)!!
else -> @Suppress("DEPRECATION") getSerializable(key) as T
}
inline fun Bundle.nullableSerializable(key: String): T? = when {
- Build.VERSION.SDK_INT >= 33 -> getSerializable(key, T::class.java)
+ Build.VERSION.SDK_INT >= 34 -> getSerializable(key, T::class.java)
else -> @Suppress("DEPRECATION") getSerializable(key) as T?
}
@Suppress("UNCHECKED_CAST")
-inline fun Bundle.parcelableArray(key: String): Array? = when {
- Build.VERSION.SDK_INT >= 33 -> getParcelableArray(key, T::class.java)
- else -> @Suppress("DEPRECATION") getParcelableArray(key) as Array?
-}
+inline fun Bundle.parcelableArray(key: String): Array? =
+ BundleCompat.getParcelableArray(this, key, T::class.java) as Array?
inline fun Intent.serializable(key: String): T = when {
- Build.VERSION.SDK_INT >= 33 -> getSerializableExtra(key, T::class.java)!!
+ Build.VERSION.SDK_INT >= 34 -> getSerializableExtra(key, T::class.java)!!
else -> @Suppress("DEPRECATION") getSerializableExtra(key) as T
}
inline fun Intent.nullableSerializable(key: String): T? = when {
- Build.VERSION.SDK_INT >= 33 -> getSerializableExtra(key, T::class.java)
+ Build.VERSION.SDK_INT >= 34 -> getSerializableExtra(key, T::class.java)
else -> @Suppress("DEPRECATION") getSerializableExtra(key) as T?
}
diff --git a/app/src/main/java/io/github/wulkanowy/utils/ExceptionExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/ExceptionExtension.kt
index 18fc10bba..d541c0a7e 100644
--- a/app/src/main/java/io/github/wulkanowy/utils/ExceptionExtension.kt
+++ b/app/src/main/java/io/github/wulkanowy/utils/ExceptionExtension.kt
@@ -3,11 +3,13 @@ package io.github.wulkanowy.utils
import android.content.res.Resources
import io.github.wulkanowy.R
import io.github.wulkanowy.sdk.exception.FeatureNotAvailableException
+import io.github.wulkanowy.sdk.scrapper.exception.AccountInactiveException
import io.github.wulkanowy.sdk.scrapper.exception.CloudflareVerificationException
import io.github.wulkanowy.sdk.scrapper.exception.FeatureDisabledException
import io.github.wulkanowy.sdk.scrapper.exception.ScrapperException
import io.github.wulkanowy.sdk.scrapper.exception.ServiceUnavailableException
import io.github.wulkanowy.sdk.scrapper.exception.VulcanException
+import io.github.wulkanowy.sdk.scrapper.login.BadCredentialsException
import io.github.wulkanowy.sdk.scrapper.login.NotLoggedInException
import io.github.wulkanowy.sdk.scrapper.login.PasswordChangeRequiredException
import okhttp3.internal.http2.StreamResetException
@@ -33,6 +35,8 @@ fun Resources.getErrorString(error: Throwable): String = when (error) {
is ServiceUnavailableException -> R.string.error_service_unavailable
is FeatureDisabledException -> R.string.error_feature_disabled
is FeatureNotAvailableException -> R.string.error_feature_not_available
+ is BadCredentialsException -> R.string.error_password_invalid
+ is AccountInactiveException -> R.string.error_account_inactive
is VulcanException -> R.string.error_unknown_uonet
is ScrapperException -> R.string.error_unknown_app
is CloudflareVerificationException -> R.string.error_cloudflare_captcha
diff --git a/app/src/main/java/io/github/wulkanowy/utils/SdkExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/SdkExtension.kt
deleted file mode 100644
index 9b6ca7060..000000000
--- a/app/src/main/java/io/github/wulkanowy/utils/SdkExtension.kt
+++ /dev/null
@@ -1,42 +0,0 @@
-package io.github.wulkanowy.utils
-
-import io.github.wulkanowy.data.db.entities.Semester
-import io.github.wulkanowy.data.db.entities.Student
-import io.github.wulkanowy.sdk.Sdk
-import timber.log.Timber
-
-fun Sdk.init(student: Student): Sdk {
- email = student.email
- password = student.password
- symbol = student.symbol
- schoolSymbol = student.schoolSymbol
- studentId = student.studentId
- classId = student.classId
- emptyCookieJarInterceptor = true
-
- if (Sdk.Mode.valueOf(student.loginMode) == Sdk.Mode.HEBE) {
- mobileBaseUrl = student.mobileBaseUrl
- } else {
- scrapperBaseUrl = student.scrapperBaseUrl
- domainSuffix = student.scrapperDomainSuffix
- loginType = Sdk.ScrapperLoginType.valueOf(student.loginType)
- }
-
- mode = Sdk.Mode.valueOf(student.loginMode)
- mobileBaseUrl = student.mobileBaseUrl
- keyId = student.certificateKey
- privatePem = student.privateKey
-
- Timber.d("Sdk in ${student.loginMode} mode reinitialized")
-
- return this
-}
-
-fun Sdk.switchSemester(semester: Semester): Sdk {
- return switchDiary(
- diaryId = semester.diaryId,
- kindergartenDiaryId = semester.kindergartenDiaryId,
- schoolYear = semester.schoolYear,
- unitId = semester.unitId,
- )
-}
diff --git a/app/src/main/java/io/github/wulkanowy/utils/WebkitCookieManagerProxy.kt b/app/src/main/java/io/github/wulkanowy/utils/WebkitCookieManagerProxy.kt
index 3d41c711c..4d2dde788 100644
--- a/app/src/main/java/io/github/wulkanowy/utils/WebkitCookieManagerProxy.kt
+++ b/app/src/main/java/io/github/wulkanowy/utils/WebkitCookieManagerProxy.kt
@@ -5,17 +5,21 @@ import java.net.CookiePolicy
import java.net.CookieStore
import java.net.HttpCookie
import java.net.URI
+import javax.inject.Inject
+import javax.inject.Singleton
import android.webkit.CookieManager as WebkitCookieManager
import java.net.CookieManager as JavaCookieManager
-class WebkitCookieManagerProxy : JavaCookieManager(null, CookiePolicy.ACCEPT_ALL) {
+@Singleton
+class WebkitCookieManagerProxy @Inject constructor() :
+ JavaCookieManager(null, CookiePolicy.ACCEPT_ALL) {
- private val webkitCookieManager: WebkitCookieManager? = getWebkitCookieManager()
+ val webkitCookieManager: WebkitCookieManager? = getCookieManager()
/**
* @see [https://stackoverflow.com/a/70354583/6695449]
*/
- private fun getWebkitCookieManager(): WebkitCookieManager? {
+ private fun getCookieManager(): WebkitCookieManager? {
return try {
WebkitCookieManager.getInstance()
} catch (e: AndroidRuntimeException) {
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 5736992bf..2a57977f1 100644
--- a/app/src/main/play/release-notes/pl-PL/default.txt
+++ b/app/src/main/play/release-notes/pl-PL/default.txt
@@ -1,5 +1,11 @@
-Wersja 2.4.1
+Wersja 2.5.1
-- drobne poprawki stabilności aplikacji i odświeżania danych
+— dodaliśmy wyświetlanie ogłoszeń
+— dodaliśmy opcję przywracania wiadomości z kosza
+— dodaliśmy opcję wyciszania nadawców wiadomości
+— naprawiliśmy opcjonalne liczenie średniej arytmetycznej, kiedy brak ocen z wagą w drugim semestrze
+— usprawniliśmy ładowanie frekwencji i planu lekcji
+— naprawiliśmy usprawiedliwianie nieobecności i autoryzację u użytkowników eduOne
+— zmieniliśmy komunikat o zmienionym haśle
Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases
diff --git a/app/src/main/res/drawable/ic_circle_notification.xml b/app/src/main/res/drawable/ic_circle_notification.xml
new file mode 100644
index 000000000..6059212cb
--- /dev/null
+++ b/app/src/main/res/drawable/ic_circle_notification.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
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/drawable/ic_menu_message_delete_forever.xml b/app/src/main/res/drawable/ic_menu_message_delete_forever.xml
new file mode 100644
index 000000000..a7b5ac53b
--- /dev/null
+++ b/app/src/main/res/drawable/ic_menu_message_delete_forever.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_menu_message_restore.xml b/app/src/main/res/drawable/ic_menu_message_restore.xml
new file mode 100644
index 000000000..5c8544f28
--- /dev/null
+++ b/app/src/main/res/drawable/ic_menu_message_restore.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_notifications_off.xml b/app/src/main/res/drawable/ic_notifications_off.xml
new file mode 100644
index 000000000..094ed75fa
--- /dev/null
+++ b/app/src/main/res/drawable/ic_notifications_off.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index d14de50a1..a9284234e 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -16,7 +16,7 @@
+ android:layout_height="wrap_content" />
@@ -67,7 +67,7 @@
android:layout_height="wrap_content"
android:editable="false"
android:focusable="false"
- android:inputType="text"
+ android:inputType="none"
tools:ignore="Deprecated" />
@@ -87,7 +87,7 @@
android:layout_height="wrap_content"
android:editable="false"
android:focusable="false"
- android:inputType="text"
+ android:inputType="none"
tools:ignore="Deprecated" />
@@ -113,7 +113,7 @@
android:id="@+id/additionalLessonDialogClose"
style="@style/Widget.Material3.Button.TextButton.Dialog"
android:layout_width="wrap_content"
- android:layout_height="36dp"
+ android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginTop="24dp"
android:layout_marginEnd="8dp"
@@ -122,6 +122,7 @@
android:insetTop="0dp"
android:insetRight="0dp"
android:insetBottom="0dp"
+ android:minHeight="36dp"
android:text="@string/all_close"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/additionalLessonDialogAdd"
@@ -131,7 +132,7 @@
android:id="@+id/additionalLessonDialogAdd"
style="@style/Widget.Material3.Button.TextButton.Dialog"
android:layout_width="wrap_content"
- android:layout_height="36dp"
+ android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginTop="24dp"
android:layout_marginBottom="24dp"
@@ -139,6 +140,7 @@
android:insetTop="0dp"
android:insetRight="0dp"
android:insetBottom="0dp"
+ android:minHeight="36dp"
android:text="@string/all_add"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
diff --git a/app/src/main/res/layout/dialog_attendance.xml b/app/src/main/res/layout/dialog_attendance.xml
index d832490a7..b9c63539c 100644
--- a/app/src/main/res/layout/dialog_attendance.xml
+++ b/app/src/main/res/layout/dialog_attendance.xml
@@ -150,7 +150,7 @@
android:id="@+id/attendanceDialogClose"
style="@style/Widget.Material3.Button.TextButton.Dialog"
android:layout_width="wrap_content"
- android:layout_height="36dp"
+ android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="24dp"
@@ -158,6 +158,7 @@
android:insetTop="0dp"
android:insetRight="0dp"
android:insetBottom="0dp"
+ android:minHeight="36dp"
android:text="@string/all_close"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
diff --git a/app/src/main/res/layout/dialog_captcha.xml b/app/src/main/res/layout/dialog_captcha.xml
index 539aa0cc9..019d89327 100644
--- a/app/src/main/res/layout/dialog_captcha.xml
+++ b/app/src/main/res/layout/dialog_captcha.xml
@@ -7,15 +7,18 @@
tools:context=".ui.modules.captcha.CaptchaDialog">
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintVertical_bias="0" />
+
+
+
+
+ app:layout_constraintTop_toBottomOf="@id/captcha_webview" />
diff --git a/app/src/main/res/layout/dialog_conference.xml b/app/src/main/res/layout/dialog_conference.xml
index 72837b819..3d33fd568 100644
--- a/app/src/main/res/layout/dialog_conference.xml
+++ b/app/src/main/res/layout/dialog_conference.xml
@@ -181,7 +181,7 @@
android:id="@+id/conferenceDialogClose"
style="@style/Widget.Material3.Button.TextButton.Dialog"
android:layout_width="wrap_content"
- android:layout_height="36dp"
+ android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="24dp"
@@ -189,6 +189,7 @@
android:insetTop="0dp"
android:insetRight="0dp"
android:insetBottom="0dp"
+ android:minHeight="36dp"
android:text="@string/all_close"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
diff --git a/app/src/main/res/layout/dialog_exam.xml b/app/src/main/res/layout/dialog_exam.xml
index 519d1531f..e2f77e1f2 100644
--- a/app/src/main/res/layout/dialog_exam.xml
+++ b/app/src/main/res/layout/dialog_exam.xml
@@ -220,7 +220,7 @@
android:id="@+id/examDialogAddToCalendar"
style="@style/Widget.Material3.Button.TextButton.Dialog"
android:layout_width="wrap_content"
- android:layout_height="36dp"
+ android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginBottom="24dp"
android:contentDescription="@string/all_add_to_calendar"
@@ -228,6 +228,7 @@
android:insetTop="0dp"
android:insetRight="0dp"
android:insetBottom="0dp"
+ android:minHeight="36dp"
android:text="@string/all_add"
app:icon="@drawable/ic_calendar_all"
app:layout_constraintBottom_toBottomOf="parent"
@@ -237,7 +238,7 @@
android:id="@+id/examDialogClose"
style="@style/Widget.Material3.Button.TextButton.Dialog"
android:layout_width="wrap_content"
- android:layout_height="36dp"
+ android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="24dp"
@@ -245,6 +246,7 @@
android:insetTop="0dp"
android:insetRight="0dp"
android:insetBottom="0dp"
+ android:minHeight="36dp"
android:text="@string/all_close"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
diff --git a/app/src/main/res/layout/dialog_grade.xml b/app/src/main/res/layout/dialog_grade.xml
index f47f61088..8606a5ce5 100644
--- a/app/src/main/res/layout/dialog_grade.xml
+++ b/app/src/main/res/layout/dialog_grade.xml
@@ -212,7 +212,7 @@
android:id="@+id/gradeDialogClose"
style="@style/Widget.Material3.Button.TextButton.Dialog"
android:layout_width="wrap_content"
- android:layout_height="36dp"
+ android:layout_height="wrap_content"
android:layout_below="@+id/gradeDialogColorValue"
android:layout_alignParentRight="true"
android:layout_marginTop="24dp"
@@ -222,6 +222,7 @@
android:insetTop="0dp"
android:insetRight="0dp"
android:insetBottom="0dp"
+ android:minHeight="36dp"
android:text="@string/all_close" />
diff --git a/app/src/main/res/layout/dialog_homework.xml b/app/src/main/res/layout/dialog_homework.xml
index 8c6cf0a76..10b719077 100644
--- a/app/src/main/res/layout/dialog_homework.xml
+++ b/app/src/main/res/layout/dialog_homework.xml
@@ -27,7 +27,7 @@
android:id="@+id/homeworkDialogRead"
style="@style/Widget.Material3.Button.TextButton.Dialog"
android:layout_width="wrap_content"
- android:layout_height="36dp"
+ android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginEnd="8dp"
android:layout_marginBottom="24dp"
@@ -35,6 +35,7 @@
android:insetTop="0dp"
android:insetRight="0dp"
android:insetBottom="0dp"
+ android:minHeight="36dp"
android:text="@string/homework_mark_as_done"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/homeworkDialogClose" />
@@ -43,13 +44,14 @@
android:id="@+id/homeworkDialogClose"
style="@style/Widget.Material3.Button.TextButton.Dialog"
android:layout_width="wrap_content"
- android:layout_height="36dp"
+ android:layout_height="wrap_content"
android:layout_marginEnd="24dp"
android:layout_marginBottom="24dp"
android:insetLeft="0dp"
android:insetTop="0dp"
android:insetRight="0dp"
android:insetBottom="0dp"
+ android:minHeight="36dp"
android:text="@string/all_close"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
diff --git a/app/src/main/res/layout/dialog_homework_add.xml b/app/src/main/res/layout/dialog_homework_add.xml
index e0ff5b749..dc7ae32d5 100644
--- a/app/src/main/res/layout/dialog_homework_add.xml
+++ b/app/src/main/res/layout/dialog_homework_add.xml
@@ -94,7 +94,7 @@
android:id="@+id/homeworkDialogClose"
style="@style/Widget.Material3.Button.TextButton.Dialog"
android:layout_width="wrap_content"
- android:layout_height="36dp"
+ android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginTop="24dp"
android:layout_marginEnd="8dp"
@@ -103,6 +103,7 @@
android:insetTop="0dp"
android:insetRight="0dp"
android:insetBottom="0dp"
+ android:minHeight="36dp"
android:text="@string/all_close"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/homeworkDialogAdd"
@@ -112,13 +113,14 @@
android:id="@+id/homeworkDialogAdd"
style="@style/Widget.Material3.Button.TextButton.Dialog"
android:layout_width="wrap_content"
- android:layout_height="36dp"
+ android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layout_marginBottom="24dp"
android:insetLeft="0dp"
android:insetTop="0dp"
android:insetRight="0dp"
android:insetBottom="0dp"
+ android:minHeight="36dp"
android:text="@string/all_add"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
diff --git a/app/src/main/res/layout/dialog_lesson_completed.xml b/app/src/main/res/layout/dialog_lesson_completed.xml
index 3a1d3fd00..fc32a252a 100644
--- a/app/src/main/res/layout/dialog_lesson_completed.xml
+++ b/app/src/main/res/layout/dialog_lesson_completed.xml
@@ -212,7 +212,7 @@
android:id="@+id/completedLessonDialogClose"
style="@style/Widget.Material3.Button.TextButton.Dialog"
android:layout_width="wrap_content"
- android:layout_height="36dp"
+ android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="24dp"
@@ -220,6 +220,7 @@
android:insetTop="0dp"
android:insetRight="0dp"
android:insetBottom="0dp"
+ android:minHeight="36dp"
android:text="@string/all_close"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
diff --git a/app/src/main/res/layout/dialog_mobile_device.xml b/app/src/main/res/layout/dialog_mobile_device.xml
index 9b81737fb..c526ed74c 100644
--- a/app/src/main/res/layout/dialog_mobile_device.xml
+++ b/app/src/main/res/layout/dialog_mobile_device.xml
@@ -18,10 +18,10 @@
android:layout_marginTop="24dp"
android:adjustViewBounds="true"
android:contentDescription="@string/mobile_device_qr"
- tools:src="@tools:sample/avatars"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toTopOf="parent" />
+ app:layout_constraintTop_toTopOf="parent"
+ tools:src="@tools:sample/avatars" />
+
+ app:constraint_referenced_ids="mobileDeviceQr,mobileDeviceDialogTokenTitle,mobileDeviceDialogTokenValue,mobileDeviceDialogSymbolTitle,mobileDeviceDialogSymbolValue,mobileDeviceDialogPinTitle,mobileDeviceDialogPinValue,mobileDeviceDialogClose"
+ tools:visibility="visible" />
+ tools:visibility="invisible" />
diff --git a/app/src/main/res/layout/dialog_note.xml b/app/src/main/res/layout/dialog_note.xml
index 9c8b18b32..3b88ea5f8 100644
--- a/app/src/main/res/layout/dialog_note.xml
+++ b/app/src/main/res/layout/dialog_note.xml
@@ -180,7 +180,7 @@
android:id="@+id/noteDialogClose"
style="@style/Widget.Material3.Button.TextButton.Dialog"
android:layout_width="wrap_content"
- android:layout_height="36dp"
+ android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="24dp"
@@ -188,6 +188,7 @@
android:insetTop="0dp"
android:insetRight="0dp"
android:insetBottom="0dp"
+ android:minHeight="36dp"
android:text="@string/all_close"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
diff --git a/app/src/main/res/layout/dialog_school_announcement.xml b/app/src/main/res/layout/dialog_school_announcement.xml
index 4e0ef556f..a771b772f 100644
--- a/app/src/main/res/layout/dialog_school_announcement.xml
+++ b/app/src/main/res/layout/dialog_school_announcement.xml
@@ -122,7 +122,7 @@
android:id="@+id/announcementDialogClose"
style="@style/Widget.Material3.Button.TextButton.Dialog"
android:layout_width="wrap_content"
- android:layout_height="36dp"
+ android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="24dp"
@@ -130,6 +130,7 @@
android:insetTop="0dp"
android:insetRight="0dp"
android:insetBottom="0dp"
+ android:minHeight="36dp"
android:text="@string/all_close"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
diff --git a/app/src/main/res/layout/dialog_timetable.xml b/app/src/main/res/layout/dialog_timetable.xml
index aeb01b3ba..de2696482 100644
--- a/app/src/main/res/layout/dialog_timetable.xml
+++ b/app/src/main/res/layout/dialog_timetable.xml
@@ -263,7 +263,7 @@
android:id="@+id/timetableDialogClose"
style="@style/Widget.Material3.Button.TextButton.Dialog"
android:layout_width="wrap_content"
- android:layout_height="36dp"
+ android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="24dp"
@@ -271,6 +271,7 @@
android:insetTop="0dp"
android:insetRight="0dp"
android:insetBottom="0dp"
+ android:minHeight="36dp"
android:text="@string/all_close"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
diff --git a/app/src/main/res/layout/fragment_attendance_calculator.xml b/app/src/main/res/layout/fragment_attendance_calculator.xml
new file mode 100644
index 000000000..346c6aecd
--- /dev/null
+++ b/app/src/main/res/layout/fragment_attendance_calculator.xml
@@ -0,0 +1,103 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/header_grade_details.xml b/app/src/main/res/layout/header_grade_details.xml
index f2ba9a8c9..e43e8993f 100644
--- a/app/src/main/res/layout/header_grade_details.xml
+++ b/app/src/main/res/layout/header_grade_details.xml
@@ -45,6 +45,9 @@
android:textColor="?android:textColorSecondary"
android:textSize="12sp"
app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toStartOf="@id/gradeHeaderPointsSum"
+ 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" />
@@ -55,8 +58,12 @@
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginTop="5dp"
+ android:ellipsize="end"
+ android:maxLines="1"
android:textColor="?android:textColorSecondary"
android:textSize="12sp"
+ app:layout_constrainedWidth="true"
+ app:layout_constraintEnd_toStartOf="@id/gradeHeaderNumber"
app:layout_constraintStart_toEndOf="@+id/gradeHeaderAverage"
app:layout_constraintTop_toBottomOf="@+id/gradeHeaderSubject"
tools:text="Points: 123/200 (61,5%)" />
@@ -67,8 +74,13 @@
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginTop="5dp"
+ android:layout_marginEnd="8dp"
+ android:ellipsize="end"
+ android:maxLines="1"
android:textColor="?android:textColorSecondary"
android:textSize="12sp"
+ app:layout_constrainedWidth="true"
+ app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/gradeHeaderPointsSum"
app:layout_constraintTop_toBottomOf="@id/gradeHeaderSubject"
tools:text="12 grades" />
@@ -85,6 +97,9 @@
android:paddingRight="5dp"
android:textColor="?colorOnPrimary"
android:textSize="14sp"
+ app:autoSizeMaxTextSize="16dp"
+ app:autoSizeMinTextSize="10dp"
+ app:autoSizeTextType="uniform"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
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_message.xml b/app/src/main/res/layout/item_message.xml
index 39fbaad01..1346c3f05 100644
--- a/app/src/main/res/layout/item_message.xml
+++ b/app/src/main/res/layout/item_message.xml
@@ -81,9 +81,9 @@
+
+
@@ -40,6 +54,7 @@
android:id="@+id/schoolAnnouncementItemContent"
android:layout_width="0dp"
android:layout_height="wrap_content"
+ android:layout_marginHorizontal="15dp"
android:layout_marginTop="5dp"
android:layout_marginBottom="15dp"
android:ellipsize="end"
@@ -47,8 +62,8 @@
android:maxLines="2"
android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintEnd_toEndOf="@+id/schoolAnnouncementItemType"
- app:layout_constraintStart_toStartOf="@id/schoolAnnouncementItemDate"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/schoolAnnouncementItemType"
tools:text="@tools:sample/lorem/random" />
diff --git a/app/src/main/res/layout/item_timetable.xml b/app/src/main/res/layout/item_timetable.xml
index 57af6f7ea..d13105229 100644
--- a/app/src/main/res/layout/item_timetable.xml
+++ b/app/src/main/res/layout/item_timetable.xml
@@ -1,7 +1,6 @@
@@ -83,13 +94,14 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
- android:layout_marginTop="0dp"
- android:layout_marginEnd="5dp"
+ android:layout_marginEnd="0dp"
+ android:ellipsize="end"
+ android:maxLines="1"
android:textColor="?android:textColorSecondary"
android:textSize="13sp"
app:layout_constraintEnd_toStartOf="@+id/timetableItemTeacher"
app:layout_constraintStart_toEndOf="@+id/timetableItemRoom"
- app:layout_constraintTop_toTopOf="@+id/timetableItemTimeFinish"
+ app:layout_constraintTop_toTopOf="@id/timetableItemTimeFinish"
tools:text="(2/2)"
tools:visibility="visible" />
@@ -98,13 +110,15 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
- android:layout_marginEnd="16dp"
+ android:layout_marginEnd="8dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?android:textColorSecondary"
android:textSize="13sp"
- app:layout_constraintBottom_toBottomOf="@+id/timetableItemNumber"
+ app:layout_constrainedWidth="true"
+ app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/timetableItemGroup"
+ app:layout_constraintTop_toTopOf="@id/timetableItemTimeFinish"
tools:text="Agata Kowalska - Błaszczyk"
tools:visibility="visible" />
@@ -118,8 +132,8 @@
android:textColor="?colorTimetableChange"
android:textSize="13sp"
app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toEndOf="@+id/timetableItemTeacher"
- app:layout_constraintTop_toTopOf="@+id/timetableItemTimeFinish"
+ app:layout_constraintStart_toEndOf="@id/timetableItemTimeFinish"
+ app:layout_constraintTop_toTopOf="@id/timetableItemTimeFinish"
tools:text="Lekcja odwołana: uczniowie zwolnieni do domu"
tools:visibility="gone" />
@@ -168,7 +182,7 @@
android:visibility="gone"
app:backgroundTint="?colorPrimary"
app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintTop_toTopOf="@id/timetableItemTimeStart"
tools:text="jeszcze 15 min"
tools:visibility="visible" />
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/subitem_dashboard_small_grade.xml b/app/src/main/res/layout/subitem_dashboard_small_grade.xml
index 6800b72e9..5d48313a3 100644
--- a/app/src/main/res/layout/subitem_dashboard_small_grade.xml
+++ b/app/src/main/res/layout/subitem_dashboard_small_grade.xml
@@ -1,5 +1,6 @@
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 @@
diff --git a/app/src/main/res/menu/context_menu_message_tab.xml b/app/src/main/res/menu/context_menu_message_tab.xml
index 36d4a8bae..013616c58 100644
--- a/app/src/main/res/menu/context_menu_message_tab.xml
+++ b/app/src/main/res/menu/context_menu_message_tab.xml
@@ -1,6 +1,13 @@