diff --git a/.gitignore b/.gitignore index 921bd0a9..980085e3 100644 --- a/.gitignore +++ b/.gitignore @@ -65,6 +65,8 @@ captures/ .idea/uiDesigner.xml .idea/runConfigurations.xml .idea/discord.xml +.idea/migrations.xml +.idea/androidTestResultsUserPreferences.xml # Keystore files *.jks diff --git a/.idea/migrations.xml b/.idea/migrations.xml deleted file mode 100644 index f8051a6f..00000000 --- a/.idea/migrations.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index ef3f1565..b8123667 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -27,8 +27,8 @@ android { testApplicationId "io.github.tests.wulkanowy" minSdkVersion 21 targetSdkVersion 34 - versionCode 138 - versionName "2.2.6" + versionCode 148 + versionName "2.4.2" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" resValue "string", "app_name", "Wulkanowy" @@ -143,7 +143,8 @@ android { resources { excludes += ['META-INF/library_release.kotlin_module', 'META-INF/library-core_release.kotlin_module', - 'META-INF/*'] + 'META-INF/LICENSE.md', + 'META-INF/LICENSE-notice.md'] } } @@ -163,8 +164,8 @@ play { defaultToAppBundles = false track = 'production' releaseStatus = ReleaseStatus.IN_PROGRESS - userFraction = 0.15d - updatePriority = 3 + userFraction = 0.99d + updatePriority = 2 enabled.set(false) } @@ -189,16 +190,16 @@ ext { android_hilt = "1.1.0" room = "2.6.1" chucker = "4.0.0" - mockk = "1.13.8" - coroutines = "1.7.3" + mockk = "1.13.9" + coroutines = "1.8.0" } dependencies { - implementation 'io.github.wulkanowy:sdk:2.3.1' + implementation 'io.github.wulkanowy:sdk:2.4.2-SNAPSHOT' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4' - implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2" + implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines" implementation 'androidx.core:core-ktx:1.12.0' @@ -222,7 +223,7 @@ dependencies { implementation "androidx.work:work-runtime:$work_manager" playImplementation "androidx.work:work-gcm:$work_manager" - implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.6.2" + implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.7.0" implementation "androidx.room:room-runtime:$room" implementation "androidx.room:room-ktx:$room" @@ -239,9 +240,10 @@ dependencies { implementation 'com.squareup.retrofit2:retrofit:2.9.0' implementation "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0" implementation "com.squareup.okhttp3:logging-interceptor:4.12.0" + implementation "com.squareup.okhttp3:okhttp-urlconnection:4.12.0" implementation "com.jakewharton.timber:timber:5.0.1" - implementation "at.favre.lib:slf4j-timber:1.0.1" + 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' @@ -250,7 +252,7 @@ dependencies { implementation 'com.fredporciuncula:flow-preferences:1.9.1' implementation 'org.apache.commons:commons-text:1.11.0' - playImplementation platform('com.google.firebase:firebase-bom:32.7.0') + playImplementation platform('com.google.firebase:firebase-bom:32.7.2') playImplementation 'com.google.firebase:firebase-analytics' playImplementation 'com.google.firebase:firebase-messaging' playImplementation 'com.google.firebase:firebase-crashlytics:' @@ -262,7 +264,7 @@ dependencies { playImplementation 'com.google.android.play:review-ktx:2.0.1' playImplementation "com.google.android.ump:user-messaging-platform:2.1.0" - hmsImplementation 'com.huawei.hms:hianalytics:6.12.0.300' + hmsImplementation 'com.huawei.hms:hianalytics:6.12.0.301' hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.9.1.303' releaseImplementation "com.github.chuckerteam.chucker:library-no-op:$chucker" diff --git a/app/schemas/io.github.wulkanowy.data.db.AppDatabase/58.json b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/58.json new file mode 100644 index 00000000..e6e71229 --- /dev/null +++ b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/58.json @@ -0,0 +1,2451 @@ +{ + "formatVersion": 1, + "database": { + "version": 58, + "identityHash": "cd1d4f8f2b6e3860fbc1de93d4f5ca42", + "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": [] + } + ], + "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, 'cd1d4f8f2b6e3860fbc1de93d4f5ca42')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.github.wulkanowy.data.db.AppDatabase/59.json b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/59.json new file mode 100644 index 00000000..a3f2e0dc --- /dev/null +++ b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/59.json @@ -0,0 +1,2501 @@ +{ + "formatVersion": 1, + "database": { + "version": 59, + "identityHash": "3bd95e40b587e8131a2a2c23aee538c1", + "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": "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, '3bd95e40b587e8131a2a2c23aee538c1')" + ] + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/io/github/wulkanowy/utils/security/ScramblerTest.kt b/app/src/androidTest/java/io/github/wulkanowy/utils/security/ScramblerTest.kt index 0c47e6bb..1b0319f6 100644 --- a/app/src/androidTest/java/io/github/wulkanowy/utils/security/ScramblerTest.kt +++ b/app/src/androidTest/java/io/github/wulkanowy/utils/security/ScramblerTest.kt @@ -14,34 +14,37 @@ import kotlin.test.assertFailsWith @RunWith(AndroidJUnit4::class) class ScramblerTest { + private val scrambler = Scrambler(ApplicationProvider.getApplicationContext()) + @Test fun encryptDecryptTest() { - assertEquals("TEST", decrypt(encrypt("TEST", - ApplicationProvider.getApplicationContext()))) + assertEquals( + "TEST", scrambler.decrypt(scrambler.encrypt("TEST")) + ) } @Test fun emptyTextEncryptTest() { assertFailsWith { - decrypt("") + scrambler.decrypt("") } assertFailsWith { - encrypt("", ApplicationProvider.getApplicationContext()) + scrambler.encrypt("") } } @Test @SdkSuppress(minSdkVersion = 18) fun emptyKeyStoreTest() { - val text = encrypt("test", ApplicationProvider.getApplicationContext()) + val text = scrambler.encrypt("test") val keyStore = KeyStore.getInstance("AndroidKeyStore") keyStore.load(null) keyStore.deleteEntry("wulkanowy_password") assertFailsWith { - decrypt(text) + scrambler.decrypt(text) } } } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 174c9a1f..f43dfdd2 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -44,6 +44,7 @@ android:networkSecurityConfig="@xml/network_security_config" android:supportsRtl="false" android:theme="@style/WulkanowyTheme" + android:resizeableActivity="true" tools:ignore="DataExtractionRules,UnusedAttribute"> Resource.dataOrNull: T? get() = when (this) { is Resource.Success -> this.data is Resource.Intermediate -> this.data - is Resource.Loading -> null - is Resource.Error -> null + else -> null + } + +val Resource.dataOrThrow: T + get() = when (this) { + is Resource.Success -> this.data + is Resource.Intermediate -> this.data + is Resource.Loading -> throw IllegalStateException("Resource is in loading state") + is Resource.Error -> throw this.error } val Resource.errorOrNull: Throwable? @@ -131,7 +148,7 @@ inline fun networkBoundResource( query().map { Resource.Success(filterResult(it)) } } catch (throwable: Throwable) { onFetchFailed(throwable) - query().map { Resource.Error(throwable) } + flowOf(Resource.Error(throwable)) } } else { query().map { Resource.Success(filterResult(it)) } @@ -165,7 +182,7 @@ inline fun networkBoundResource( query().map { Resource.Success(mapResult(it)) } } catch (throwable: Throwable) { onFetchFailed(throwable) - query().map { Resource.Error(throwable) } + flowOf(Resource.Error(throwable)) } } else { query().map { Resource.Success(mapResult(it)) } 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 48a2942c..8e5841fe 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 @@ -1,11 +1,126 @@ package io.github.wulkanowy.data.db import android.content.Context -import androidx.room.* +import androidx.room.AutoMigration +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase import androidx.room.RoomDatabase.JournalMode.TRUNCATE -import io.github.wulkanowy.data.db.dao.* -import io.github.wulkanowy.data.db.entities.* -import io.github.wulkanowy.data.db.migrations.* +import androidx.room.TypeConverters +import io.github.wulkanowy.data.db.dao.AdminMessageDao +import io.github.wulkanowy.data.db.dao.AttendanceDao +import io.github.wulkanowy.data.db.dao.AttendanceSummaryDao +import io.github.wulkanowy.data.db.dao.CompletedLessonsDao +import io.github.wulkanowy.data.db.dao.ConferenceDao +import io.github.wulkanowy.data.db.dao.ExamDao +import io.github.wulkanowy.data.db.dao.GradeDao +import io.github.wulkanowy.data.db.dao.GradeDescriptiveDao +import io.github.wulkanowy.data.db.dao.GradePartialStatisticsDao +import io.github.wulkanowy.data.db.dao.GradePointsStatisticsDao +import io.github.wulkanowy.data.db.dao.GradeSemesterStatisticsDao +import io.github.wulkanowy.data.db.dao.GradeSummaryDao +import io.github.wulkanowy.data.db.dao.HomeworkDao +import io.github.wulkanowy.data.db.dao.LuckyNumberDao +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.NoteDao +import io.github.wulkanowy.data.db.dao.NotificationDao +import io.github.wulkanowy.data.db.dao.RecipientDao +import io.github.wulkanowy.data.db.dao.SchoolAnnouncementDao +import io.github.wulkanowy.data.db.dao.SchoolDao +import io.github.wulkanowy.data.db.dao.SemesterDao +import io.github.wulkanowy.data.db.dao.StudentDao +import io.github.wulkanowy.data.db.dao.StudentInfoDao +import io.github.wulkanowy.data.db.dao.SubjectDao +import io.github.wulkanowy.data.db.dao.TeacherDao +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.AdminMessage +import io.github.wulkanowy.data.db.entities.Attendance +import io.github.wulkanowy.data.db.entities.AttendanceSummary +import io.github.wulkanowy.data.db.entities.CompletedLesson +import io.github.wulkanowy.data.db.entities.Conference +import io.github.wulkanowy.data.db.entities.Exam +import io.github.wulkanowy.data.db.entities.Grade +import io.github.wulkanowy.data.db.entities.GradeDescriptive +import io.github.wulkanowy.data.db.entities.GradePartialStatistics +import io.github.wulkanowy.data.db.entities.GradePointsStatistics +import io.github.wulkanowy.data.db.entities.GradeSemesterStatistics +import io.github.wulkanowy.data.db.entities.GradeSummary +import io.github.wulkanowy.data.db.entities.Homework +import io.github.wulkanowy.data.db.entities.LuckyNumber +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.Note +import io.github.wulkanowy.data.db.entities.Notification +import io.github.wulkanowy.data.db.entities.Recipient +import io.github.wulkanowy.data.db.entities.School +import io.github.wulkanowy.data.db.entities.SchoolAnnouncement +import io.github.wulkanowy.data.db.entities.Semester +import io.github.wulkanowy.data.db.entities.Student +import io.github.wulkanowy.data.db.entities.StudentInfo +import io.github.wulkanowy.data.db.entities.Subject +import io.github.wulkanowy.data.db.entities.Teacher +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.db.migrations.Migration10 +import io.github.wulkanowy.data.db.migrations.Migration11 +import io.github.wulkanowy.data.db.migrations.Migration12 +import io.github.wulkanowy.data.db.migrations.Migration13 +import io.github.wulkanowy.data.db.migrations.Migration14 +import io.github.wulkanowy.data.db.migrations.Migration15 +import io.github.wulkanowy.data.db.migrations.Migration16 +import io.github.wulkanowy.data.db.migrations.Migration17 +import io.github.wulkanowy.data.db.migrations.Migration18 +import io.github.wulkanowy.data.db.migrations.Migration19 +import io.github.wulkanowy.data.db.migrations.Migration2 +import io.github.wulkanowy.data.db.migrations.Migration20 +import io.github.wulkanowy.data.db.migrations.Migration21 +import io.github.wulkanowy.data.db.migrations.Migration22 +import io.github.wulkanowy.data.db.migrations.Migration23 +import io.github.wulkanowy.data.db.migrations.Migration24 +import io.github.wulkanowy.data.db.migrations.Migration25 +import io.github.wulkanowy.data.db.migrations.Migration26 +import io.github.wulkanowy.data.db.migrations.Migration27 +import io.github.wulkanowy.data.db.migrations.Migration28 +import io.github.wulkanowy.data.db.migrations.Migration29 +import io.github.wulkanowy.data.db.migrations.Migration3 +import io.github.wulkanowy.data.db.migrations.Migration30 +import io.github.wulkanowy.data.db.migrations.Migration31 +import io.github.wulkanowy.data.db.migrations.Migration32 +import io.github.wulkanowy.data.db.migrations.Migration33 +import io.github.wulkanowy.data.db.migrations.Migration34 +import io.github.wulkanowy.data.db.migrations.Migration35 +import io.github.wulkanowy.data.db.migrations.Migration36 +import io.github.wulkanowy.data.db.migrations.Migration37 +import io.github.wulkanowy.data.db.migrations.Migration38 +import io.github.wulkanowy.data.db.migrations.Migration39 +import io.github.wulkanowy.data.db.migrations.Migration4 +import io.github.wulkanowy.data.db.migrations.Migration40 +import io.github.wulkanowy.data.db.migrations.Migration41 +import io.github.wulkanowy.data.db.migrations.Migration42 +import io.github.wulkanowy.data.db.migrations.Migration43 +import io.github.wulkanowy.data.db.migrations.Migration44 +import io.github.wulkanowy.data.db.migrations.Migration46 +import io.github.wulkanowy.data.db.migrations.Migration49 +import io.github.wulkanowy.data.db.migrations.Migration5 +import io.github.wulkanowy.data.db.migrations.Migration50 +import io.github.wulkanowy.data.db.migrations.Migration51 +import io.github.wulkanowy.data.db.migrations.Migration53 +import io.github.wulkanowy.data.db.migrations.Migration54 +import io.github.wulkanowy.data.db.migrations.Migration55 +import io.github.wulkanowy.data.db.migrations.Migration57 +import io.github.wulkanowy.data.db.migrations.Migration58 +import io.github.wulkanowy.data.db.migrations.Migration6 +import io.github.wulkanowy.data.db.migrations.Migration7 +import io.github.wulkanowy.data.db.migrations.Migration8 +import io.github.wulkanowy.data.db.migrations.Migration9 import io.github.wulkanowy.utils.AppInfo import javax.inject.Singleton @@ -41,7 +156,8 @@ import javax.inject.Singleton TimetableHeader::class, SchoolAnnouncement::class, Notification::class, - AdminMessage::class + AdminMessage::class, + GradeDescriptive::class, ], autoMigrations = [ AutoMigration(from = 44, to = 45), @@ -51,6 +167,8 @@ import javax.inject.Singleton AutoMigration(from = 54, to = 55, spec = Migration55::class), AutoMigration(from = 55, to = 56), AutoMigration(from = 56, to = 57, spec = Migration57::class), + AutoMigration(from = 57, to = 58, spec = Migration58::class), + AutoMigration(from = 58, to = 59), ], version = AppDatabase.VERSION_SCHEMA, exportSchema = true @@ -59,7 +177,7 @@ import javax.inject.Singleton abstract class AppDatabase : RoomDatabase() { companion object { - const val VERSION_SCHEMA = 57 + const val VERSION_SCHEMA = 59 fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf( Migration2(), @@ -184,4 +302,6 @@ abstract class AppDatabase : RoomDatabase() { abstract val notificationDao: NotificationDao abstract val adminMessagesDao: AdminMessageDao + + abstract val gradeDescriptiveDao: GradeDescriptiveDao } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/dao/GradeDescriptiveDao.kt b/app/src/main/java/io/github/wulkanowy/data/db/dao/GradeDescriptiveDao.kt new file mode 100644 index 00000000..6282c080 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/db/dao/GradeDescriptiveDao.kt @@ -0,0 +1,15 @@ +package io.github.wulkanowy.data.db.dao + +import androidx.room.Dao +import androidx.room.Query +import io.github.wulkanowy.data.db.entities.GradeDescriptive +import kotlinx.coroutines.flow.Flow +import javax.inject.Singleton + +@Singleton +@Dao +interface GradeDescriptiveDao : BaseDao { + + @Query("SELECT * FROM GradesDescriptive WHERE semester_id = :semesterId AND student_id = :studentId") + fun loadAll(semesterId: Int, studentId: Int): Flow> +} diff --git a/app/src/main/java/io/github/wulkanowy/data/db/dao/StudentDao.kt b/app/src/main/java/io/github/wulkanowy/data/db/dao/StudentDao.kt index d7847c24..d9326ff6 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/dao/StudentDao.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/dao/StudentDao.kt @@ -1,11 +1,16 @@ package io.github.wulkanowy.data.db.dao -import androidx.room.* +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Transaction +import androidx.room.Update import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.StudentName import io.github.wulkanowy.data.db.entities.StudentNickAndAvatar -import io.github.wulkanowy.data.db.entities.StudentWithSemesters import javax.inject.Singleton @Singleton @@ -47,6 +52,9 @@ abstract class StudentDao { @Query("UPDATE Students SET is_current = 0") abstract suspend fun resetCurrent() + @Query("DELETE FROM Students WHERE email = :email AND user_name = :userName") + abstract suspend fun deleteByEmailAndUserName(email: String, userName: String) + @Transaction open suspend fun switchCurrent(id: Long) { resetCurrent() 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 b4b7379f..40d97ea9 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/AdminMessage.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/AdminMessage.kt index 875c2a3a..0c8f1a5d 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/entities/AdminMessage.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/AdminMessage.kt @@ -37,6 +37,9 @@ data class AdminMessage( @ColumnInfo(name = "types", defaultValue = "[]") val types: List = emptyList(), - @ColumnInfo(name = "is_dismissible") - val isDismissible: Boolean = false + @ColumnInfo(name = "is_ok_visible", defaultValue = "0") + val isOkVisible: Boolean = false, + + @ColumnInfo(name = "is_x_visible", defaultValue = "0") + val isXVisible: Boolean = false ) diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/GradeDescriptive.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/GradeDescriptive.kt new file mode 100644 index 00000000..9aec9599 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/GradeDescriptive.kt @@ -0,0 +1,27 @@ +package io.github.wulkanowy.data.db.entities + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import java.io.Serializable + +@Entity(tableName = "GradesDescriptive") +data class GradeDescriptive( + + @ColumnInfo(name = "semester_id") + val semesterId: Int, + + @ColumnInfo(name = "student_id") + val studentId: Int, + + val subject: String, + + val description: String, +) : Serializable { + + @PrimaryKey(autoGenerate = true) + var id: Long = 0 + + @ColumnInfo(name = "is_notified") + var isNotified: Boolean = true +} diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration58.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration58.kt new file mode 100644 index 00000000..c440d58d --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration58.kt @@ -0,0 +1,10 @@ +package io.github.wulkanowy.data.db.migrations + +import androidx.room.DeleteColumn +import androidx.room.migration.AutoMigrationSpec + +@DeleteColumn( + tableName = "AdminMessages", + columnName = "is_dismissible", +) +class Migration58 : AutoMigrationSpec diff --git a/app/src/main/java/io/github/wulkanowy/data/mappers/GradeMapper.kt b/app/src/main/java/io/github/wulkanowy/data/mappers/GradeMapper.kt index 178de682..66e92217 100644 --- a/app/src/main/java/io/github/wulkanowy/data/mappers/GradeMapper.kt +++ b/app/src/main/java/io/github/wulkanowy/data/mappers/GradeMapper.kt @@ -1,10 +1,12 @@ package io.github.wulkanowy.data.mappers import io.github.wulkanowy.data.db.entities.Grade +import io.github.wulkanowy.data.db.entities.GradeDescriptive import io.github.wulkanowy.data.db.entities.GradeSummary import io.github.wulkanowy.data.db.entities.Semester -import io.github.wulkanowy.sdk.pojo.GradeSummary as SdkGradeSummary import io.github.wulkanowy.sdk.pojo.Grade as SdkGrade +import io.github.wulkanowy.sdk.pojo.GradeDescriptive as SdkGradeDescriptive +import io.github.wulkanowy.sdk.pojo.GradeSummary as SdkGradeSummary fun List.mapToEntities(semester: Semester) = map { Grade( @@ -40,3 +42,15 @@ fun List.mapToEntities(semester: Semester) = map { average = it.average ) } + +@JvmName("mapGradeDescriptiveToEntities") +fun List.mapToEntities(semester: Semester) = map { + GradeDescriptive( + semesterId = semester.semesterId, + studentId = semester.studentId, + subject = it.subject, + description = it.description + ) +} + + 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 e8609c1a..39043533 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 @@ -10,17 +10,17 @@ import io.github.wulkanowy.data.networkBoundResource import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.pojo.Absent import io.github.wulkanowy.ui.modules.dashboard.DashboardItem -import io.github.wulkanowy.utils.* import kotlinx.coroutines.Dispatchers +import io.github.wulkanowy.utils.AutoRefreshHelper +import io.github.wulkanowy.utils.getRefreshKey +import io.github.wulkanowy.utils.init +import io.github.wulkanowy.utils.monday +import io.github.wulkanowy.utils.sunday +import io.github.wulkanowy.utils.switchSemester +import io.github.wulkanowy.utils.uniqueSubtract import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.asFlow -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flatMapConcat -import kotlinx.coroutines.flow.toList import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.withContext -import timber.log.Timber import java.time.LocalDate import java.time.LocalDateTime import java.time.LocalTime @@ -88,7 +88,7 @@ class AttendanceRepository @Inject constructor( } sdk.init(student) - .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) + .switchSemester(semester) .getAttendance(start.monday, end.sunday) .filter { attendance -> !badAttendanceHidden || @@ -138,7 +138,7 @@ class AttendanceRepository @Inject constructor( ) } sdk.init(student) - .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) + .switchSemester(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 8e070913..6bdcf9d7 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 @@ -9,6 +9,7 @@ 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 @@ -40,7 +41,7 @@ class AttendanceSummaryRepository @Inject constructor( query = { attendanceDb.loadAll(semester.diaryId, semester.studentId, subjectId) }, fetch = { sdk.init(student) - .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) + .switchSemester(semester) .getAttendanceSummary(subjectId) .mapToEntities(semester, subjectId) }, 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 8f393cad..1579ae62 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 @@ -48,7 +48,7 @@ class CompletedLessonsRepository @Inject constructor( }, fetch = { sdk.init(student) - .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) + .switchSemester(semester) .getCompletedLessons(start.monday, end.sunday) .mapToEntities(semester) }, 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 83204cab..7eb37f0b 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 @@ -10,6 +10,7 @@ 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 @@ -46,7 +47,7 @@ class ConferenceRepository @Inject constructor( }, fetch = { sdk.init(student) - .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) + .switchSemester(semester) .getConferences() .mapToEntities(semester) .filter { it.date >= startDate } 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 013c0951..96026a55 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 @@ -7,7 +7,13 @@ 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.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 import java.time.LocalDate @@ -51,7 +57,7 @@ class ExamRepository @Inject constructor( }, fetch = { sdk.init(student) - .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) + .switchSemester(semester) .getExams(start.startExamsDay, start.endExamsDay) .mapToEntities(semester) }, @@ -67,14 +73,16 @@ class ExamRepository @Inject constructor( filterResult = { it.filter { item -> item.date in start..end } } ) - fun getExamsFromDatabase(semester: Semester, start: LocalDate): Flow> { - return examDb.loadAll( - diaryId = semester.diaryId, - studentId = semester.studentId, - from = start.startExamsDay, - end = start.endExamsDay - ) - } + fun getExamsFromDatabase( + semester: Semester, + start: LocalDate, + end: LocalDate + ): Flow> = examDb.loadAll( + diaryId = semester.diaryId, + studentId = semester.studentId, + from = start, + end = end, + ) suspend fun updateExam(exam: List) = examDb.updateAll(exam) } 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 8b80d47d..c328be44 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,8 +1,10 @@ package io.github.wulkanowy.data.repositories import io.github.wulkanowy.data.db.dao.GradeDao +import io.github.wulkanowy.data.db.dao.GradeDescriptiveDao import io.github.wulkanowy.data.db.dao.GradeSummaryDao import io.github.wulkanowy.data.db.entities.Grade +import io.github.wulkanowy.data.db.entities.GradeDescriptive import io.github.wulkanowy.data.db.entities.GradeSummary import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student @@ -10,7 +12,12 @@ import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.data.networkBoundResource import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.ui.modules.dashboard.DashboardItem -import io.github.wulkanowy.utils.* +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 import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map @@ -24,14 +31,13 @@ import javax.inject.Singleton class GradeRepository @Inject constructor( private val gradeDb: GradeDao, private val gradeSummaryDb: GradeSummaryDao, + private val gradeDescriptiveDb: GradeDescriptiveDao, private val sdk: Sdk, private val refreshHelper: AutoRefreshHelper, private val preferencesRepository: PreferencesRepository ) { private val saveFetchResultMutex = Mutex() - private val cacheKey = "grade" - private fun loadGrades(semesterId: Int, studentId: Int): Flow> { val badGradesHidden = preferencesRepository .selectedHiddenSettingTiles @@ -56,22 +62,28 @@ class GradeRepository @Inject constructor( //When details is empty and summary is not, app will not use summary cache - edge case it.first.isEmpty() }, - shouldFetch = { (details, summaries) -> - val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, semester)) - details.isEmpty() || summaries.isEmpty() || forceRefresh || isExpired + shouldFetch = { (details, summaries, descriptive) -> + val isExpired = + refreshHelper.shouldBeRefreshed(getRefreshKey(GRADE_CACHE_KEY, semester)) + details.isEmpty() || (summaries.isEmpty() && descriptive.isEmpty()) || forceRefresh || isExpired }, query = { val detailsFlow = loadGrades(semester.semesterId, semester.studentId) val summaryFlow = gradeSummaryDb.loadAll(semester.semesterId, semester.studentId) - detailsFlow.combine(summaryFlow) { details, summaries -> details to summaries } + val descriptiveFlow = + gradeDescriptiveDb.loadAll(semester.semesterId, semester.studentId) + + combine(detailsFlow, summaryFlow, descriptiveFlow) { details, summaries, descriptive -> + Triple(details, summaries, descriptive) + } }, fetch = { val badGradesHidden = preferencesRepository .selectedHiddenSettingTiles .contains(DashboardItem.HiddenSettingTile.BAD_GRADES) - val (details, summary) = sdk.init(student) - .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) + val (details, summary, descriptive) = sdk.init(student) + .switchSemester(semester) .getGrades(semester.semesterId) val censoredDetails = if (badGradesHidden) { @@ -80,16 +92,32 @@ class GradeRepository @Inject constructor( details } - censoredDetails.mapToEntities(semester) to summary.mapToEntities(semester) + Triple( + censoredDetails.mapToEntities(semester), + summary.mapToEntities(semester), + descriptive.mapToEntities(semester) + ) }, - saveFetchResult = { (oldDetails, oldSummary), (newDetails, newSummary) -> + saveFetchResult = { (oldDetails, oldSummary, oldDescriptive), (newDetails, newSummary, newDescriptive) -> refreshGradeDetails(student, oldDetails, newDetails, notify) refreshGradeSummaries(oldSummary, newSummary, notify) + refreshGradeDescriptions(oldDescriptive, newDescriptive, notify) - refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester)) + refreshHelper.updateLastRefreshTimestamp(getRefreshKey(GRADE_CACHE_KEY, semester)) } ) + private suspend fun refreshGradeDescriptions( + old: List, + new: List, + notify: Boolean + ) { + gradeDescriptiveDb.deleteAll(old uniqueSubtract new) + gradeDescriptiveDb.insertAll((new uniqueSubtract old).onEach { + if (notify) it.isNotified = false + }) + } + private suspend fun refreshGradeDetails( student: Student, oldGrades: List, @@ -157,6 +185,10 @@ class GradeRepository @Inject constructor( return gradeSummaryDb.loadAll(semester.semesterId, semester.studentId) } + fun getGradesDescriptiveFromDatabase(semester: Semester): Flow> { + return gradeDescriptiveDb.loadAll(semester.semesterId, semester.studentId) + } + suspend fun updateGrade(grade: Grade) { return gradeDb.updateAll(listOf(grade)) } @@ -168,4 +200,13 @@ class GradeRepository @Inject constructor( suspend fun updateGradesSummary(gradesSummary: List) { return gradeSummaryDb.updateAll(gradesSummary) } + + suspend fun updateGradesDescriptive(gradesDescriptive: List) { + return gradeDescriptiveDb.updateAll(gradesDescriptive) + } + + private companion object { + + private const val GRADE_CACHE_KEY = "grade" + } } 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 9fa06c49..23d7b858 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 @@ -16,6 +16,7 @@ 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.* @@ -56,7 +57,7 @@ class GradeStatisticsRepository @Inject constructor( query = { gradePartialStatisticsDb.loadAll(semester.semesterId, semester.studentId) }, fetch = { sdk.init(student) - .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) + .switchSemester(semester) .getGradesPartialStatistics(semester.semesterId) .mapToEntities(semester) }, @@ -101,7 +102,7 @@ class GradeStatisticsRepository @Inject constructor( query = { gradeSemesterStatisticsDb.loadAll(semester.semesterId, semester.studentId) }, fetch = { sdk.init(student) - .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) + .switchSemester(semester) .getGradesSemesterStatistics(semester.semesterId) .mapToEntities(semester) }, @@ -157,7 +158,7 @@ class GradeStatisticsRepository @Inject constructor( query = { gradePointsStatisticsDb.loadAll(semester.semesterId, semester.studentId) }, fetch = { sdk.init(student) - .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) + .switchSemester(semester) .getGradesPointsStatistics(semester.semesterId) .mapToEntities(semester) }, 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 f564824d..010cf845 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 @@ -7,7 +7,13 @@ 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.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 import javax.inject.Inject @@ -50,7 +56,7 @@ class HomeworkRepository @Inject constructor( }, fetch = { sdk.init(student) - .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) + .switchSemester(semester) .getHomework(start.monday, end.sunday) .mapToEntities(semester) }, 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 87e8410f..4ff4517d 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 @@ -35,12 +35,15 @@ class LuckyNumberRepository @Inject constructor( fetch = { sdk.init(student).getLuckyNumber(student.schoolShortName)?.mapToEntity(student) }, - saveFetchResult = { old, new -> - if (new != old) { - old?.let { luckyNumberDb.deleteAll(listOfNotNull(it)) } - luckyNumberDb.insertAll(listOfNotNull((new?.apply { - if (notify) isNotified = false - }))) + 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) } } ) 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 07c6959e..412f9e7f 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 @@ -12,6 +12,7 @@ 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 @@ -42,7 +43,7 @@ class MobileDeviceRepository @Inject constructor( query = { mobileDb.loadAll(student.userLoginId) }, fetch = { sdk.init(student) - .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) + .switchSemester(semester) .getRegisteredDevices() .mapToEntities(student) }, @@ -56,7 +57,7 @@ class MobileDeviceRepository @Inject constructor( suspend fun unregisterDevice(student: Student, semester: Semester, device: MobileDevice) { sdk.init(student) - .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) + .switchSemester(semester) .unregisterDevice(device.deviceId) mobileDb.deleteAll(listOf(device)) @@ -64,7 +65,7 @@ class MobileDeviceRepository @Inject constructor( suspend fun getToken(student: Student, semester: Semester): MobileDeviceToken { return sdk.init(student) - .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) + .switchSemester(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 41a6bedb..ac83bc22 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 @@ -56,7 +56,7 @@ class NoteRepository @Inject constructor( .contains(DashboardItem.HiddenSettingTile.NOTES) sdk.init(student) - .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) + .switchSemester(semester) .getNotes() .filter { !notesHidden } .mapToEntities(semester) 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 7972ed08..f757ef04 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 @@ -9,6 +9,7 @@ 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 @@ -40,7 +41,7 @@ class SchoolRepository @Inject constructor( query = { schoolDb.load(semester.studentId, semester.classId) }, fetch = { sdk.init(student) - .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) + .switchSemester(semester) .getSchool() .mapToEntity(semester) }, 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 9c642934..216a8c11 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 @@ -11,6 +11,7 @@ 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 @@ -42,11 +43,7 @@ class SchoolsRepository @Inject constructor( val schoolInfo = sdk .init(student.copy(password = loginData.password)) - .switchDiary( - diaryId = semester.diaryId, - kindergartenDiaryId = semester.kindergartenDiaryId, - schoolYear = semester.schoolYear - ) + .switchSemester(semester) .getSchool() schoolsService.logLoginEvent( 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 efc82a77..d6cd25c8 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 @@ -7,6 +7,7 @@ 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 @@ -30,7 +31,7 @@ class StudentInfoRepository @Inject constructor( query = { studentInfoDao.loadStudentInfo(student.studentId) }, fetch = { sdk.init(student) - .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) + .switchSemester(semester) .getStudentInfo().mapToEntity(semester) }, saveFetchResult = { old, 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 2e04224a..e063840c 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,8 +1,6 @@ package io.github.wulkanowy.data.repositories -import android.content.Context import androidx.room.withTransaction -import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.data.db.AppDatabase import io.github.wulkanowy.data.db.dao.SemesterDao import io.github.wulkanowy.data.db.dao.StudentDao @@ -17,20 +15,20 @@ 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.decrypt -import io.github.wulkanowy.utils.security.encrypt +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 @Singleton class StudentRepository @Inject constructor( - @ApplicationContext private val context: Context, private val dispatchers: DispatchersProvider, private val studentDb: StudentDao, private val semesterDb: SemesterDao, private val sdk: Sdk, - private val appDatabase: AppDatabase + private val appDatabase: AppDatabase, + private val scrambler: Scrambler, ) { suspend fun isCurrentStudentSet() = studentDb.loadCurrent()?.isCurrent ?: false @@ -68,7 +66,7 @@ class StudentRepository @Inject constructor( student = student.apply { if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE) { student.password = withContext(dispatchers.io) { - decrypt(student.password) + scrambler.decrypt(student.password) } } }, @@ -86,7 +84,7 @@ class StudentRepository @Inject constructor( }.apply { if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE) { student.password = withContext(dispatchers.io) { - decrypt(student.password) + scrambler.decrypt(student.password) } } } @@ -96,7 +94,7 @@ class StudentRepository @Inject constructor( if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE) { student.password = withContext(dispatchers.io) { - decrypt(student.password) + scrambler.decrypt(student.password) } } return student @@ -107,7 +105,7 @@ class StudentRepository @Inject constructor( if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE) { student.password = withContext(dispatchers.io) { - decrypt(student.password) + scrambler.decrypt(student.password) } } return student @@ -120,7 +118,7 @@ class StudentRepository @Inject constructor( it.apply { if (Sdk.Mode.valueOf(it.loginMode) != Sdk.Mode.HEBE) { password = withContext(dispatchers.io) { - encrypt(password, context) + scrambler.encrypt(password) } } } @@ -152,12 +150,12 @@ class StudentRepository @Inject constructor( suspend fun authorizePermission(student: Student, semester: Semester, pesel: String) = sdk.init(student) - .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) + .switchSemester(semester) .authorizePermission(pesel) suspend fun refreshStudentName(student: Student, semester: Semester) { val newCurrentApiStudent = sdk.init(student) - .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) + .switchSemester(semester) .getCurrentStudent() ?: return val studentName = StudentName( @@ -166,4 +164,15 @@ class StudentRepository @Inject constructor( studentDb.update(studentName) } + + suspend fun deleteStudentsAssociatedWithAccount(student: Student) { + studentDb.deleteByEmailAndUserName(student.email, student.userName) + } + + suspend fun clearAll() { + withContext(dispatchers.io) { + scrambler.clearKeyPair() + appDatabase.clearAllTables() + } + } } 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 3926122b..98cb181a 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 @@ -9,6 +9,7 @@ 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 @@ -39,8 +40,9 @@ class SubjectRepository @Inject constructor( query = { subjectDao.loadAll(semester.diaryId, semester.studentId) }, fetch = { sdk.init(student) - .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) - .getSubjects().mapToEntities(semester) + .switchSemester(semester) + .getSubjects() + .mapToEntities(semester) }, saveFetchResult = { old, new -> subjectDao.deleteAll(old uniqueSubtract new) 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 4e3b40f9..42698f92 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 @@ -9,6 +9,7 @@ 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 @@ -39,7 +40,7 @@ class TeacherRepository @Inject constructor( query = { teacherDb.loadAll(semester.studentId, semester.classId) }, fetch = { sdk.init(student) - .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) + .switchSemester(semester) .getTeachers() .mapToEntities(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 136fb8d5..acbd02d1 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 @@ -3,13 +3,23 @@ package io.github.wulkanowy.data.repositories 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.init +import io.github.wulkanowy.utils.monday +import io.github.wulkanowy.utils.sunday +import io.github.wulkanowy.utils.switchSemester +import io.github.wulkanowy.utils.uniqueSubtract import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.sync.Mutex @@ -65,7 +75,7 @@ class TimetableRepository @Inject constructor( query = { getFullTimetableFromDatabase(student, semester, start, end) }, fetch = { val timetableFull = sdk.init(student) - .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) + .switchSemester(semester) .getTimetable(start.monday, end.sunday) timetableFull.mapToEntities(semester) @@ -121,12 +131,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) { 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 new file mode 100644 index 00000000..ffd00574 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/domain/timetable/IsStudentHasLessonsOnWeekendUseCase.kt @@ -0,0 +1,26 @@ +package io.github.wulkanowy.domain.timetable + +import io.github.wulkanowy.data.db.entities.Semester +import io.github.wulkanowy.data.repositories.TimetableRepository +import io.github.wulkanowy.utils.monday +import io.github.wulkanowy.utils.sunday +import java.time.LocalDate +import javax.inject.Inject + +class IsStudentHasLessonsOnWeekendUseCase @Inject constructor( + private val timetableRepository: TimetableRepository, + private val isWeekendHasLessonsUseCase: IsWeekendHasLessonsUseCase, +) { + + suspend operator fun invoke( + semester: Semester, + currentDate: LocalDate = LocalDate.now(), + ): Boolean { + val lessons = timetableRepository.getTimetableFromDatabase( + semester = semester, + start = currentDate.monday, + end = currentDate.sunday, + ) + return isWeekendHasLessonsUseCase(lessons) + } +} diff --git a/app/src/main/java/io/github/wulkanowy/domain/timetable/IsWeekendHasLessonsUseCase.kt b/app/src/main/java/io/github/wulkanowy/domain/timetable/IsWeekendHasLessonsUseCase.kt new file mode 100644 index 00000000..908c9df7 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/domain/timetable/IsWeekendHasLessonsUseCase.kt @@ -0,0 +1,17 @@ +package io.github.wulkanowy.domain.timetable + +import io.github.wulkanowy.data.db.entities.Timetable +import java.time.DayOfWeek +import javax.inject.Inject + +class IsWeekendHasLessonsUseCase @Inject constructor() { + + operator fun invoke( + lessons: List, + ): Boolean = lessons.any { + it.date.dayOfWeek in listOf( + DayOfWeek.SATURDAY, + DayOfWeek.SUNDAY, + ) + } +} diff --git a/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationSchedulerHelper.kt b/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationSchedulerHelper.kt index 42078d03..aae7882f 100644 --- a/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationSchedulerHelper.kt +++ b/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationSchedulerHelper.kt @@ -65,8 +65,6 @@ class TimetableNotificationSchedulerHelper @Inject constructor( range = lesson.start..lesson.end, requestCode = getRequestCode(lesson.start, studentId) ) - - Timber.d("TimetableNotification canceled: type 1 & 2, subject: ${lesson.subject}, start: ${lesson.start}, student: $studentId") } } } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewGradeNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewGradeNotification.kt index 9ab16857..232ba672 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewGradeNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewGradeNotification.kt @@ -4,6 +4,7 @@ import android.content.Context import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Grade +import io.github.wulkanowy.data.db.entities.GradeDescriptive import io.github.wulkanowy.data.db.entities.GradeSummary import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.pojos.GroupNotificationData @@ -19,7 +20,7 @@ class NewGradeNotification @Inject constructor( suspend fun notifyDetails(items: List, student: Student) { val notificationDataList = items - .filter { !listOf("1", "1+", "2", "2-", "2+").contains(it.entry) } + .filter { !it.isNotified } .map { NotificationData( title = context.getPlural(R.plurals.grade_new_items, 1), @@ -89,4 +90,28 @@ class NewGradeNotification @Inject constructor( appNotificationManager.sendMultipleNotifications(groupNotificationData, student) } + + suspend fun notifyDescriptive(items: List, student: Student) { + val notificationDataList = items.map { + NotificationData( + title = context.getPlural(R.plurals.grade_new_items_descriptive, 1), + content = "${it.subject}: ${it.description}", + destination = Destination.Grade, + ) + } + + val groupNotificationData = GroupNotificationData( + notificationDataList = notificationDataList, + title = context.getPlural(R.plurals.grade_new_items_descriptive, items.size), + content = context.getPlural( + R.plurals.grade_notify_new_items_descriptive, + items.size, + items.size + ), + destination = Destination.Grade, + type = NotificationType.NEW_GRADE_DESCRIPTIVE + ) + + appNotificationManager.sendMultipleNotifications(groupNotificationData, student) + } } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NotificationType.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NotificationType.kt index 023ae2e4..4e7f2735 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NotificationType.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NotificationType.kt @@ -37,6 +37,10 @@ enum class NotificationType( channel = NewGradesChannel.CHANNEL_ID, icon = R.drawable.ic_stat_grade, ), + NEW_GRADE_DESCRIPTIVE( + channel = NewGradesChannel.CHANNEL_ID, + icon = R.drawable.ic_stat_grade, + ), NEW_HOMEWORK( channel = NewHomeworkChannel.CHANNEL_ID, icon = R.drawable.ic_more_homework, diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/works/AttendanceWork.kt b/app/src/main/java/io/github/wulkanowy/services/sync/works/AttendanceWork.kt index 657f6963..4fc09749 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/works/AttendanceWork.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/works/AttendanceWork.kt @@ -16,17 +16,24 @@ class AttendanceWork @Inject constructor( ) : Work { override suspend fun doWork(student: Student, semester: Semester, notify: Boolean) { + val startDate = now().previousOrSameSchoolDay + val endDate = startDate.plusDays(7) + attendanceRepository.getAttendance( student = student, semester = semester, - start = now().previousOrSameSchoolDay, - end = now().previousOrSameSchoolDay, + start = startDate, + end = endDate, forceRefresh = true, notify = notify, ) .waitForResult() - attendanceRepository.getAttendanceFromDatabase(semester, now().minusDays(7), now()) + attendanceRepository.getAttendanceFromDatabase( + semester = semester, + start = startDate, + end = endDate, + ) .first() .filterNot { it.isNotified } .let { diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/works/ExamWork.kt b/app/src/main/java/io/github/wulkanowy/services/sync/works/ExamWork.kt index 7071bce2..4b0b1bdb 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/works/ExamWork.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/works/ExamWork.kt @@ -5,6 +5,8 @@ import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.repositories.ExamRepository import io.github.wulkanowy.data.waitForResult import io.github.wulkanowy.services.sync.notifications.NewExamNotification +import io.github.wulkanowy.utils.endExamsDay +import io.github.wulkanowy.utils.startExamsDay import kotlinx.coroutines.flow.first import java.time.LocalDate.now import javax.inject.Inject @@ -15,16 +17,24 @@ class ExamWork @Inject constructor( ) : Work { override suspend fun doWork(student: Student, semester: Semester, notify: Boolean) { + val startDate = now().startExamsDay + val endDate = startDate.endExamsDay + examRepository.getExams( student = student, semester = semester, - start = now(), - end = now(), + start = startDate, + end = endDate, forceRefresh = true, notify = notify, ).waitForResult() - examRepository.getExamsFromDatabase(semester, now()).first() + examRepository.getExamsFromDatabase( + semester = semester, + start = startDate, + end = endDate, + ) + .first() .filter { !it.isNotified }.let { if (it.isNotEmpty()) newExamNotification.notify(it, student) diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/works/GradeWork.kt b/app/src/main/java/io/github/wulkanowy/services/sync/works/GradeWork.kt index ba21b860..b62ad94b 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/works/GradeWork.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/works/GradeWork.kt @@ -45,5 +45,15 @@ class GradeWork @Inject constructor( grade.isFinalGradeNotified = true }) } + + gradeRepository.getGradesDescriptiveFromDatabase(semester).first() + .filter { !it.isNotified } + .let { + if (it.isNotEmpty()) newGradeNotification.notifyDescriptive(it, student) + + gradeRepository.updateGradesDescriptive(it.onEach { grade -> + grade.isNotified = true + }) + } } } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/works/HomeworkWork.kt b/app/src/main/java/io/github/wulkanowy/services/sync/works/HomeworkWork.kt index 4cfe27d0..ddff3af7 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/works/HomeworkWork.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/works/HomeworkWork.kt @@ -5,7 +5,9 @@ import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.repositories.HomeworkRepository import io.github.wulkanowy.data.waitForResult import io.github.wulkanowy.services.sync.notifications.NewHomeworkNotification +import io.github.wulkanowy.utils.monday import io.github.wulkanowy.utils.nextOrSameSchoolDay +import io.github.wulkanowy.utils.sunday import kotlinx.coroutines.flow.first import java.time.LocalDate.now import javax.inject.Inject @@ -16,16 +18,24 @@ class HomeworkWork @Inject constructor( ) : Work { override suspend fun doWork(student: Student, semester: Semester, notify: Boolean) { + val startDate = now().nextOrSameSchoolDay.monday + val endDate = startDate.sunday + homeworkRepository.getHomework( student = student, semester = semester, - start = now().nextOrSameSchoolDay, - end = now().nextOrSameSchoolDay, + start = startDate, + end = endDate, forceRefresh = true, notify = notify, ).waitForResult() - homeworkRepository.getHomeworkFromDatabase(semester, now(), now().plusDays(7)).first() + homeworkRepository.getHomeworkFromDatabase( + semester = semester, + start = startDate, + end = endDate + ) + .first() .filter { !it.isNotified }.let { if (it.isNotEmpty()) newHomeworkNotification.notify(it, student) 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 29b1f13c..2d10d925 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 @@ -16,18 +15,24 @@ class TimetableWork @Inject constructor( ) : Work { override suspend fun doWork(student: Student, semester: Semester, notify: Boolean) { + val startDate = now().nextOrSameSchoolDay + val endDate = startDate.plusDays(7) + timetableRepository.getTimetable( student = student, semester = semester, - start = now().nextOrSameSchoolDay, - end = now().nextOrSameSchoolDay, + start = startDate, + end = endDate, forceRefresh = true, notify = notify, ) .waitForResult() - timetableRepository.getTimetableFromDatabase(semester, now(), now().plusDays(7)) - .first() + timetableRepository.getTimetableFromDatabase( + semester = semester, + start = startDate, + end = endDate, + ) .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 f622209a..29996db7 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 @@ -11,6 +11,7 @@ import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar.LENGTH_LONG import io.github.wulkanowy.R import io.github.wulkanowy.ui.modules.auth.AuthDialog +import io.github.wulkanowy.ui.modules.captcha.CaptchaDialog import io.github.wulkanowy.ui.modules.login.LoginActivity import io.github.wulkanowy.utils.FragmentLifecycleLogger import io.github.wulkanowy.utils.getThemeAttrColor @@ -68,11 +69,24 @@ abstract class BaseActivity, VB : ViewBinding> : } else Toast.makeText(this, text, Toast.LENGTH_LONG).show() } - override fun showExpiredDialog() { + override fun showExpiredCredentialsDialog() { + MaterialAlertDialogBuilder(this) + .setTitle(R.string.main_expired_credentials_title) + .setMessage(R.string.main_expired_credentials_description) + .setPositiveButton(R.string.main_log_in) { _, _ -> presenter.onConfirmExpiredCredentialsSelected() } + .setNegativeButton(android.R.string.cancel) { _, _ -> } + .show() + } + + override fun onCaptchaVerificationRequired(url: String?) { + CaptchaDialog.newInstance(url).show(supportFragmentManager, "captcha_dialog") + } + + override fun showDecryptionFailedDialog() { MaterialAlertDialogBuilder(this) .setTitle(R.string.main_session_expired) .setMessage(R.string.main_session_relogin) - .setPositiveButton(R.string.main_log_in) { _, _ -> presenter.onExpiredLoginSelected() } + .setPositiveButton(R.string.main_log_in) { _, _ -> presenter.onConfirmDecryptionFailedSelected() } .setNegativeButton(android.R.string.cancel) { _, _ -> } .show() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/BaseDialogFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/base/BaseDialogFragment.kt index 84540b1c..e63887b8 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/BaseDialogFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/BaseDialogFragment.kt @@ -8,7 +8,6 @@ import android.widget.Toast import androidx.fragment.app.DialogFragment import androidx.viewbinding.ViewBinding import com.google.android.material.elevation.SurfaceColors -import io.github.wulkanowy.ui.modules.auth.AuthDialog import io.github.wulkanowy.utils.AnalyticsHelper import io.github.wulkanowy.utils.lifecycleAwareVariable import javax.inject.Inject @@ -28,8 +27,16 @@ abstract class BaseDialogFragment : DialogFragment(), BaseView Toast.makeText(context, text, Toast.LENGTH_LONG).show() } - override fun showExpiredDialog() { - (activity as? BaseActivity<*, *>)?.showExpiredDialog() + override fun showExpiredCredentialsDialog() { + (activity as? BaseActivity<*, *>)?.showExpiredCredentialsDialog() + } + + override fun onCaptchaVerificationRequired(url: String?) { + (activity as? BaseActivity<*, *>)?.onCaptchaVerificationRequired(url) + } + + override fun showDecryptionFailedDialog() { + (activity as? BaseActivity<*, *>)?.showDecryptionFailedDialog() } override fun openClearLoginView() { @@ -41,7 +48,7 @@ abstract class BaseDialogFragment : DialogFragment(), BaseView } override fun showAuthDialog() { - AuthDialog.newInstance().show(childFragmentManager, "auth_dialog") + (activity as? BaseActivity<*, *>)?.showAuthDialog() } override fun showErrorDetailsDialog(error: Throwable) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/BaseFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/base/BaseFragment.kt index b25346a7..ba346131 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/BaseFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/BaseFragment.kt @@ -7,7 +7,6 @@ import androidx.viewbinding.ViewBinding import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar.LENGTH_LONG import io.github.wulkanowy.R -import io.github.wulkanowy.ui.modules.auth.AuthDialog import io.github.wulkanowy.utils.lifecycleAwareVariable abstract class BaseFragment(@LayoutRes layoutId: Int) : Fragment(layoutId), @@ -39,12 +38,20 @@ abstract class BaseFragment(@LayoutRes layoutId: Int) : Fragme } } - override fun showExpiredDialog() { - (activity as? BaseActivity<*, *>)?.showExpiredDialog() + override fun showExpiredCredentialsDialog() { + (activity as? BaseActivity<*, *>)?.showExpiredCredentialsDialog() + } + + override fun onCaptchaVerificationRequired(url: String?) { + (activity as? BaseActivity<*, *>)?.onCaptchaVerificationRequired(url) + } + + override fun showDecryptionFailedDialog() { + (activity as? BaseActivity<*, *>)?.showDecryptionFailedDialog() } override fun showAuthDialog() { - AuthDialog.newInstance().show(childFragmentManager, "auth_dialog") + (activity as? BaseActivity<*, *>)?.showAuthDialog() } override fun openClearLoginView() { diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/BasePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/base/BasePresenter.kt index 2d913103..d4cb20ca 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/BasePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/BasePresenter.kt @@ -28,20 +28,38 @@ open class BasePresenter( this.view = view errorHandler.apply { showErrorMessage = view::showError - onSessionExpired = view::showExpiredDialog + onExpiredCredentials = view::showExpiredCredentialsDialog + onCaptchaVerificationRequired = view::onCaptchaVerificationRequired + onDecryptionFailed = view::showDecryptionFailedDialog onNoCurrentStudent = view::openClearLoginView onPasswordChangeRequired = view::showChangePasswordSnackbar onAuthorizationRequired = view::showAuthDialog } } - fun onExpiredLoginSelected() { - Timber.i("Attempt to switch the student after the session expires") + fun onConfirmDecryptionFailedSelected() { + Timber.i("Attempt to clear all data") + + presenterScope.launch { + runCatching { studentRepository.clearAll() } + .onFailure { + Timber.i("Clear data result: An exception occurred") + errorHandler.dispatch(it) + } + .onSuccess { + Timber.i("Clear data result: Open login view") + view?.openClearLoginView() + } + } + } + + fun onConfirmExpiredCredentialsSelected() { + Timber.i("Attempt to delete students associated with the account and switch to new student") presenterScope.launch { runCatching { val student = studentRepository.getCurrentStudent(false) - studentRepository.logoutStudent(student) + studentRepository.deleteStudentsAssociatedWithAccount(student) val students = studentRepository.getSavedStudents(false) if (students.isNotEmpty()) { @@ -50,11 +68,11 @@ open class BasePresenter( } } .onFailure { - Timber.i("Switch student result: An exception occurred") + Timber.i("Delete students result: An exception occurred") errorHandler.dispatch(it) } .onSuccess { - Timber.i("Switch student result: Open login view") + Timber.i("Delete students result: Open login view") view?.openClearLoginView() } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/BaseView.kt b/app/src/main/java/io/github/wulkanowy/ui/base/BaseView.kt index b31737e2..88d5754f 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/BaseView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/BaseView.kt @@ -6,7 +6,11 @@ interface BaseView { fun showMessage(text: String) - fun showExpiredDialog() + fun showExpiredCredentialsDialog() + + fun onCaptchaVerificationRequired(url: String?) + + fun showDecryptionFailedDialog() fun showAuthDialog() 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 0a41a47b..7109f1ff 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 @@ -4,6 +4,7 @@ import android.content.Context import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.data.exceptions.NoCurrentStudentException import io.github.wulkanowy.sdk.scrapper.exception.AuthorizationRequiredException +import io.github.wulkanowy.sdk.scrapper.exception.CloudflareVerificationException import io.github.wulkanowy.sdk.scrapper.login.BadCredentialsException import io.github.wulkanowy.sdk.scrapper.login.PasswordChangeRequiredException import io.github.wulkanowy.utils.getErrorString @@ -15,7 +16,9 @@ open class ErrorHandler @Inject constructor(@ApplicationContext protected val co var showErrorMessage: (String, Throwable) -> Unit = { _, _ -> } - var onSessionExpired: () -> Unit = {} + var onExpiredCredentials: () -> Unit = {} + + var onDecryptionFailed: () -> Unit = {} var onNoCurrentStudent: () -> Unit = {} @@ -23,24 +26,33 @@ open class ErrorHandler @Inject constructor(@ApplicationContext protected val co var onAuthorizationRequired: () -> Unit = {} + var onCaptchaVerificationRequired: (url: String?) -> Unit = {} + fun dispatch(error: Throwable) { Timber.e(error, "An exception occurred while the Wulkanowy was running") proceed(error) } protected open fun proceed(error: Throwable) { - showErrorMessage(context.resources.getErrorString(error), error) + showDefaultMessage(error) when (error) { is PasswordChangeRequiredException -> onPasswordChangeRequired(error.redirectUrl) - is ScramblerException, is BadCredentialsException -> onSessionExpired() + is ScramblerException -> onDecryptionFailed() + is BadCredentialsException -> onExpiredCredentials() is NoCurrentStudentException -> onNoCurrentStudent() is AuthorizationRequiredException -> onAuthorizationRequired() + is CloudflareVerificationException -> onCaptchaVerificationRequired(error.originalUrl) } } + fun showDefaultMessage(error: Throwable) { + showErrorMessage(context.resources.getErrorString(error), error) + } + open fun clear() { showErrorMessage = { _, _ -> } - onSessionExpired = {} + onExpiredCredentials = {} + onDecryptionFailed = {} onNoCurrentStudent = {} onPasswordChangeRequired = {} onAuthorizationRequired = {} 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 39f376f6..4e9baac3 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.graphics.Typeface import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -10,6 +11,7 @@ import io.github.wulkanowy.data.db.entities.Attendance import io.github.wulkanowy.data.enums.SentExcuseStatus import io.github.wulkanowy.databinding.ItemAttendanceBinding import io.github.wulkanowy.utils.descriptionRes +import io.github.wulkanowy.utils.getThemeAttrColor import io.github.wulkanowy.utils.isExcusableOrNotExcused import javax.inject.Inject @@ -39,7 +41,33 @@ class AttendanceAdapter @Inject constructor() : root.context.getString(R.string.all_no_data) } attendanceItemDescription.setText(item.descriptionRes) - attendanceItemAlert.isVisible = item.let { it.absence && !it.excused } + + attendanceItemDescription.setTextColor( + root.context.getThemeAttrColor( + when { + item.absence && !item.excused -> R.attr.colorAttendanceAbsence + item.lateness && !item.excused -> R.attr.colorAttendanceLateness + else -> android.R.attr.textColorSecondary + } + ) + ) + + if (item.exemption || item.excused) { + attendanceItemDescription.setTypeface(null, Typeface.BOLD) + } else { + attendanceItemDescription.setTypeface(null, Typeface.NORMAL) + } + + attendanceItemAlert.isVisible = + item.let { (it.absence && !it.excused) || (it.lateness && !it.excused) } + + attendanceItemAlert.setColorFilter(root.context.getThemeAttrColor( + when{ + item.absence && !item.excused -> R.attr.colorAttendanceAbsence + item.lateness && !item.excused -> R.attr.colorAttendanceLateness + else -> android.R.attr.colorPrimary + } + )) attendanceItemNumber.visibility = View.GONE attendanceItemExcuseInfo.visibility = View.GONE attendanceItemExcuseCheckbox.visibility = View.GONE @@ -54,10 +82,12 @@ class AttendanceAdapter @Inject constructor() : attendanceItemExcuseInfo.visibility = View.VISIBLE attendanceItemAlert.visibility = View.INVISIBLE } + SentExcuseStatus.DENIED -> { attendanceItemExcuseInfo.setImageResource(R.drawable.ic_excuse_denied) attendanceItemExcuseInfo.visibility = View.VISIBLE } + else -> { if (item.isExcusableOrNotExcused && excuseActionMode) { attendanceItemNumber.visibility = View.GONE diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceDialog.kt index c0026bee..63503313 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceDialog.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceDialog.kt @@ -6,10 +6,12 @@ import android.view.View import androidx.core.os.bundleOf import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint +import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Attendance import io.github.wulkanowy.databinding.DialogAttendanceBinding import io.github.wulkanowy.ui.base.BaseDialogFragment import io.github.wulkanowy.utils.descriptionRes +import io.github.wulkanowy.utils.getThemeAttrColor import io.github.wulkanowy.utils.serializable import io.github.wulkanowy.utils.toFormattedString @@ -44,6 +46,16 @@ class AttendanceDialog : BaseDialogFragment() { with(binding) { attendanceDialogSubjectValue.text = attendance.subject attendanceDialogDescriptionValue.setText(attendance.descriptionRes) + attendanceDialogDescriptionValue.setTextColor( + root.context.getThemeAttrColor( + when { + attendance.absence && !attendance.excused -> R.attr.colorAttendanceAbsence + attendance.lateness && !attendance.excused -> R.attr.colorAttendanceLateness + else -> android.R.attr.textColorSecondary + } + ) + ) + attendanceDialogDateValue.text = attendance.date.toFormattedString() attendanceDialogNumberValue.text = attendance.number.toString() attendanceDialogClose.setOnClickListener { dismiss() } 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 31cc058e..47f106bf 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 @@ -12,6 +12,7 @@ import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.utils.* +import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.onEach import timber.log.Timber import java.time.DayOfWeek @@ -205,7 +206,7 @@ class AttendancePresenter @Inject constructor( val semester = semesterRepository.getCurrentSemester(student) - checkInitialAndCurrentDate(student, semester) + checkInitialAndCurrentDate(semester) attendanceRepository.getAttendance( student = student, semester = semester, @@ -261,15 +262,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) } @@ -311,6 +310,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) @@ -323,6 +323,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/auth/AuthPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/auth/AuthPresenter.kt index 8f579712..3c061f49 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 new file mode 100644 index 00000000..ed8293a9 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/captcha/CaptchaDialog.kt @@ -0,0 +1,86 @@ +package io.github.wulkanowy.ui.modules.captcha + +import android.annotation.SuppressLint +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.webkit.WebView +import android.webkit.WebViewClient +import androidx.core.os.bundleOf +import dagger.hilt.android.AndroidEntryPoint +import io.github.wulkanowy.R +import io.github.wulkanowy.databinding.DialogCaptchaBinding +import io.github.wulkanowy.sdk.Sdk +import io.github.wulkanowy.ui.base.BaseDialogFragment +import timber.log.Timber +import javax.inject.Inject + +@AndroidEntryPoint +class CaptchaDialog : BaseDialogFragment() { + + @Inject + lateinit var sdk: Sdk + + private var webView: WebView? = null + + companion object { + const val CAPTCHA_SUCCESS = "captcha_success" + private const val CAPTCHA_URL = "captcha_url" + private const val CAPTCHA_CHECK_JS = "document.getElementById('challenge-running') == null" + + fun newInstance(url: String?): CaptchaDialog { + return CaptchaDialog().apply { + arguments = bundleOf(CAPTCHA_URL to url) + } + } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View = DialogCaptchaBinding.inflate(inflater).apply { binding = this }.root + + @SuppressLint("SetJavaScriptEnabled") + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + isCancelable = false + binding.captchaRefresh.setOnClickListener { + binding.captchaWebview.loadUrl(arguments?.getString(CAPTCHA_URL).orEmpty()) + } + binding.captchaClose.setOnClickListener { dismiss() } + + with(binding.captchaWebview) { + webView = this + with(settings) { + javaScriptEnabled = true + userAgentString = sdk.userAgent + } + + webViewClient = object : WebViewClient() { + override fun onPageFinished(view: WebView?, url: String?) { + view?.evaluateJavascript(CAPTCHA_CHECK_JS) { + if (it == "true") { + onChallengeAccepted() + } + } + } + } + + loadUrl(arguments?.getString(CAPTCHA_URL).orEmpty()) + } + } + + private fun onChallengeAccepted() { + runCatching { parentFragmentManager.setFragmentResult(CAPTCHA_SUCCESS, bundleOf()) } + .onFailure { Timber.e(it) } + showMessage(getString(R.string.captcha_verified_message)) + dismissAllowingStateLoss() + } + + override fun onDestroy() { + webView?.destroy() + super.onDestroy() + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardFragment.kt index ce17c763..b7a0796c 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardFragment.kt @@ -18,8 +18,10 @@ import io.github.wulkanowy.databinding.FragmentDashboardBinding import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.modules.account.accountdetails.AccountDetailsFragment import io.github.wulkanowy.ui.modules.attendance.summary.AttendanceSummaryFragment +import io.github.wulkanowy.ui.modules.captcha.CaptchaDialog.Companion.CAPTCHA_SUCCESS import io.github.wulkanowy.ui.modules.conference.ConferenceFragment import io.github.wulkanowy.ui.modules.dashboard.adapters.DashboardAdapter +import io.github.wulkanowy.ui.modules.dashboard.viewholders.AdminMessageViewHolder import io.github.wulkanowy.ui.modules.exam.ExamFragment import io.github.wulkanowy.ui.modules.grade.GradeFragment import io.github.wulkanowy.ui.modules.homework.HomeworkFragment @@ -30,7 +32,12 @@ import io.github.wulkanowy.ui.modules.message.MessageFragment import io.github.wulkanowy.ui.modules.notificationscenter.NotificationsCenterFragment import io.github.wulkanowy.ui.modules.schoolannouncement.SchoolAnnouncementFragment import io.github.wulkanowy.ui.modules.timetable.TimetableFragment -import io.github.wulkanowy.utils.* +import io.github.wulkanowy.utils.capitalise +import io.github.wulkanowy.utils.dpToPx +import io.github.wulkanowy.utils.getErrorString +import io.github.wulkanowy.utils.getThemeAttrColor +import io.github.wulkanowy.utils.openInternetBrowser +import io.github.wulkanowy.utils.toFormattedString import java.time.LocalDate import javax.inject.Inject @@ -57,6 +64,9 @@ class DashboardFragment : BaseFragment(R.layout.fragme return ((recyclerWidth - margin) / resources.displayMetrics.density).toInt() } + override val isViewEmpty + get() = dashboardAdapter.itemCount == 0 + companion object { fun newInstance() = DashboardFragment() @@ -72,6 +82,13 @@ class DashboardFragment : BaseFragment(R.layout.fragme super.onViewCreated(view, savedInstanceState) binding = FragmentDashboardBinding.bind(view) presenter.onAttachView(this) + initializeCaptchaResultObserver() + } + + private fun initializeCaptchaResultObserver() { + childFragmentManager.setFragmentResultListener(CAPTCHA_SUCCESS, this) { _, _ -> + presenter.onRetryAfterCaptcha() + } } override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { @@ -182,8 +199,17 @@ class DashboardFragment : BaseFragment(R.layout.fragme binding.dashboardRecycler.isVisible = show } - override fun showErrorView(show: Boolean) { + override fun showErrorView(show: Boolean, adminMessageItem: DashboardItem.AdminMessages?) { binding.dashboardErrorContainer.isVisible = show + binding.dashboardErrorAdminMessage.root.isVisible = adminMessageItem != null + + if (adminMessageItem != null) { + AdminMessageViewHolder( + binding = binding.dashboardErrorAdminMessage, + onAdminMessageDismissClickListener = presenter::onAdminMessageDismissed, + onAdminMessageClickListener = presenter::onAdminMessageSelected, + ).bind(adminMessageItem.adminMessage) + } } override fun setErrorDetails(error: Throwable) { 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 281f2562..bda74a4b 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 @@ -24,11 +24,13 @@ import io.github.wulkanowy.data.repositories.SemesterRepository import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.repositories.TimetableRepository import io.github.wulkanowy.domain.adminmessage.GetAppropriateAdminMessageUseCase +import io.github.wulkanowy.domain.timetable.IsStudentHasLessonsOnWeekendUseCase import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.utils.AdsHelper import io.github.wulkanowy.utils.calculatePercentage import io.github.wulkanowy.utils.nextOrSameSchoolDay +import io.github.wulkanowy.utils.sunday import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.combine @@ -56,6 +58,7 @@ class DashboardPresenter @Inject constructor( private val messageRepository: MessageRepository, private val attendanceSummaryRepository: AttendanceSummaryRepository, private val timetableRepository: TimetableRepository, + private val isStudentHasLessonsOnWeekendUseCase: IsStudentHasLessonsOnWeekendUseCase, private val homeworkRepository: HomeworkRepository, private val examRepository: ExamRepository, private val conferenceRepository: ConferenceRepository, @@ -239,6 +242,14 @@ class DashboardPresenter @Inject constructor( loadData(selectedDashboardTiles, forceRefresh = true) } + fun onRetryAfterCaptcha() { + view?.run { + showErrorView(false) + showProgress(true) + } + loadData(selectedDashboardTiles, forceRefresh = true) + } + fun onViewReselected() { Timber.i("Dashboard view is reselected") view?.run { @@ -320,7 +331,7 @@ class DashboardPresenter @Inject constructor( ) { luckyNumberResource, messageResource, attendanceResource -> val resList = listOf(luckyNumberResource, messageResource, attendanceResource) - DashboardItem.HorizontalGroup( + resList to DashboardItem.HorizontalGroup( isLoading = resList.any { it is Resource.Loading }, error = resList.map { it.errorOrNull }.let { errors -> if (errors.all { it != null }) { @@ -345,9 +356,9 @@ class DashboardPresenter @Inject constructor( ) }) } - .filterNot { it.isLoading && forceRefresh } + .filterNot { (_, it) -> it.isLoading && forceRefresh } .distinctUntilChanged() - .onEach { + .onEach { (_, it) -> updateData(it, forceRefresh) if (it.isLoading) { @@ -365,7 +376,7 @@ class DashboardPresenter @Inject constructor( ) errorHandler.dispatch(it) } - .launch("horizontal_group ${if (forceRefresh) "-forceRefresh" else ""}") + .launchWithUniqueRefreshJob("horizontal_group", forceRefresh) } private fun loadGrades(student: Student, forceRefresh: Boolean) { @@ -399,7 +410,7 @@ class DashboardPresenter @Inject constructor( subjectWithGrades = it.dataOrNull, gradeTheme = preferencesRepository.gradeColorTheme, isLoading = true - ), forceRefresh + ), false ) if (!it.dataOrNull.isNullOrEmpty()) { @@ -431,14 +442,17 @@ class DashboardPresenter @Inject constructor( private fun loadLessons(student: Student, forceRefresh: Boolean) { flatResourceFlow { val semester = semesterRepository.getCurrentSemester(student) - val date = LocalDate.now() + val date = when (isStudentHasLessonsOnWeekendUseCase(semester)) { + true -> LocalDate.now() + else -> LocalDate.now().nextOrSameSchoolDay + } timetableRepository.getTimetable( student = student, semester = semester, start = date, - end = date.plusDays(1), - forceRefresh = forceRefresh + end = date.sunday, + forceRefresh = forceRefresh, ) } .onEach { @@ -448,7 +462,7 @@ class DashboardPresenter @Inject constructor( if (forceRefresh) return@onEach updateData( DashboardItem.Lessons(it.dataOrNull, isLoading = true), - forceRefresh + false ) if (!it.dataOrNull?.lessons.isNullOrEmpty()) { @@ -505,7 +519,7 @@ class DashboardPresenter @Inject constructor( val data = it.dataOrNull.orEmpty() updateData( DashboardItem.Homework(data, isLoading = true), - forceRefresh + false ) if (data.isNotEmpty()) { @@ -539,7 +553,7 @@ class DashboardPresenter @Inject constructor( if (forceRefresh) return@onEach updateData( DashboardItem.Announcements(it.dataOrNull.orEmpty(), isLoading = true), - forceRefresh + false ) if (!it.dataOrNull.isNullOrEmpty()) { @@ -582,7 +596,7 @@ class DashboardPresenter @Inject constructor( if (forceRefresh) return@onEach updateData( DashboardItem.Exams(it.dataOrNull.orEmpty(), isLoading = true), - forceRefresh + false ) if (!it.dataOrNull.isNullOrEmpty()) { @@ -623,7 +637,7 @@ class DashboardPresenter @Inject constructor( if (forceRefresh) return@onEach updateData( DashboardItem.Conferences(it.dataOrNull.orEmpty(), isLoading = true), - forceRefresh + false ) if (!it.dataOrNull.isNullOrEmpty()) { @@ -658,7 +672,7 @@ class DashboardPresenter @Inject constructor( is Resource.Loading -> { Timber.i("Loading dashboard admin message data started") if (forceRefresh) return@onEach - updateData(DashboardItem.AdminMessages(), forceRefresh) + updateData(DashboardItem.AdminMessages(), false) } is Resource.Success -> { @@ -688,7 +702,7 @@ class DashboardPresenter @Inject constructor( private fun loadAds(forceRefresh: Boolean) { presenterScope.launch { if (!forceRefresh) { - updateData(DashboardItem.Ads(), forceRefresh) + updateData(DashboardItem.Ads(), false) } val dashboardAdItem = @@ -809,6 +823,8 @@ class DashboardPresenter @Inject constructor( val filteredItems = itemsLoadedList.filterNot { it.type == DashboardItem.Type.ACCOUNT || it.type == DashboardItem.Type.ADMIN_MESSAGE } + val dataLoadedAdminMessageItem = + itemsLoadedList.find { it.type == DashboardItem.Type.ADMIN_MESSAGE && it.isDataLoaded } as DashboardItem.AdminMessages? val isAccountItemError = itemsLoadedList.find { it.type == DashboardItem.Type.ACCOUNT }?.error != null val isGeneralError = @@ -830,7 +846,7 @@ class DashboardPresenter @Inject constructor( showRefresh(false) if ((forceRefresh && wasGeneralError) || !forceRefresh) { showContent(false) - showErrorView(true) + showErrorView(true, dataLoadedAdminMessageItem) setErrorDetails(lastError) } } @@ -858,6 +874,28 @@ class DashboardPresenter @Inject constructor( onEach { if (it is Resource.Success) { cancelJobs(jobName) + } else if (it is Resource.Error) { + cancelJobs(jobName) + } + }.launch(jobName) + } else { + launch(jobName) + } + } + + @JvmName("launchWithUniqueRefreshJobHorizontalGroup") + private fun Flow>, *>>.launchWithUniqueRefreshJob( + name: String, + forceRefresh: Boolean + ) { + val jobName = if (forceRefresh) "$name-forceRefresh" else name + + if (forceRefresh) { + onEach { (resources, _) -> + if (resources.all { it is Resource.Success<*> }) { + cancelJobs(jobName) + } else if (resources.any { it is Resource.Error<*> }) { + cancelJobs(jobName) } }.launch(jobName) } else { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardView.kt index 76788543..56a0a773 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardView.kt @@ -6,6 +6,8 @@ interface DashboardView : BaseView { val tileWidth: Int + val isViewEmpty: Boolean + fun initView() fun updateData(data: List) @@ -18,7 +20,7 @@ interface DashboardView : BaseView { fun showRefresh(show: Boolean) - fun showErrorView(show: Boolean) + fun showErrorView(show: Boolean, adminMessageItem: DashboardItem.AdminMessages? = null) fun setErrorDetails(error: Throwable) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/viewholders/AdminMessageViewHolder.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/viewholders/AdminMessageViewHolder.kt index 81099801..1e0f0bdb 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/viewholders/AdminMessageViewHolder.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/viewholders/AdminMessageViewHolder.kt @@ -7,7 +7,6 @@ import androidx.recyclerview.widget.RecyclerView import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.AdminMessage import io.github.wulkanowy.databinding.ItemDashboardAdminMessageBinding -import io.github.wulkanowy.ui.modules.dashboard.DashboardItem import io.github.wulkanowy.utils.getThemeAttrColor class AdminMessageViewHolder( @@ -25,9 +24,11 @@ class AdminMessageViewHolder( context.getThemeAttrColor(R.attr.colorMessageHigh) to context.getThemeAttrColor(R.attr.colorOnMessageHigh) } + "MEDIUM" -> { context.getThemeAttrColor(R.attr.colorMessageMedium) to Color.BLACK } + else -> null to context.getThemeAttrColor(R.attr.colorOnSurface) } @@ -37,11 +38,16 @@ class AdminMessageViewHolder( dashboardAdminMessageItemDescription.text = item.content dashboardAdminMessageItemDescription.setTextColor(textColor) dashboardAdminMessageItemIcon.setColorFilter(textColor) - dashboardAdminMessageItemDismiss.isVisible = item.isDismissible + dashboardAdminMessageItemDismiss.isVisible = item.isOkVisible + dashboardAdminMessageItemClose.isVisible = item.isXVisible dashboardAdminMessageItemDismiss.setTextColor(textColor) + dashboardAdminMessageItemClose.imageTintList = ColorStateList.valueOf(textColor) dashboardAdminMessageItemDismiss.setOnClickListener { onAdminMessageDismissClickListener(item) } + dashboardAdminMessageItemClose.setOnClickListener { + onAdminMessageDismissClickListener(item) + } root.setCardBackgroundColor(backgroundColor?.let { ColorStateList.valueOf(it) }) item.destinationUrl?.let { url -> diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/debug/DebugFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/DebugFragment.kt index 000916b1..9db01a30 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/debug/DebugFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/DebugFragment.kt @@ -2,6 +2,7 @@ package io.github.wulkanowy.ui.modules.debug import android.os.Bundle import android.view.View +import android.webkit.CookieManager import androidx.recyclerview.widget.LinearLayoutManager import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R @@ -58,6 +59,10 @@ class DebugFragment : BaseFragment(R.layout.fragment_debug (activity as? MainActivity)?.pushView(NotificationDebugFragment.newInstance()) } + override fun clearWebkitCookies() { + CookieManager.getInstance().removeAllCookies(null) + } + override fun onDestroyView() { presenter.onDetachView() super.onDestroyView() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/debug/DebugPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/DebugPresenter.kt index 67ac8886..816b5985 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/debug/DebugPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/DebugPresenter.kt @@ -15,6 +15,7 @@ class DebugPresenter @Inject constructor( val items = listOf( DebugItem(R.string.logviewer_title), DebugItem(R.string.notification_debug_title), + DebugItem(R.string.debug_cookies_clear), ) override fun onAttachView(view: DebugView) { @@ -31,6 +32,7 @@ class DebugPresenter @Inject constructor( when (item.title) { R.string.logviewer_title -> view?.openLogViewer() R.string.notification_debug_title -> view?.openNotificationsDebug() + R.string.debug_cookies_clear -> view?.clearWebkitCookies() else -> Timber.d("Unknown debug item: $item") } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/debug/DebugView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/DebugView.kt index 9396ec6a..792d63d9 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/debug/DebugView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/DebugView.kt @@ -11,4 +11,6 @@ interface DebugView : BaseView { fun openLogViewer() fun openNotificationsDebug() + + fun clearWebkitCookies() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/NotificationDebugPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/NotificationDebugPresenter.kt index d0dfcd69..cdd186b9 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/NotificationDebugPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/NotificationDebugPresenter.kt @@ -18,6 +18,7 @@ import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.ui.modules.debug.notification.mock.debugAttendanceItems import io.github.wulkanowy.ui.modules.debug.notification.mock.debugConferenceItems import io.github.wulkanowy.ui.modules.debug.notification.mock.debugExamItems +import io.github.wulkanowy.ui.modules.debug.notification.mock.debugGradeDescriptiveItems import io.github.wulkanowy.ui.modules.debug.notification.mock.debugGradeDetailsItems import io.github.wulkanowy.ui.modules.debug.notification.mock.debugGradeSummaryItems import io.github.wulkanowy.ui.modules.debug.notification.mock.debugHomeworkItems @@ -55,6 +56,14 @@ class NotificationDebugPresenter @Inject constructor( NotificationDebugItem(R.string.grade_summary_final_grade) { n -> withStudent { newGradeNotification.notifyFinal(debugGradeSummaryItems.take(n), it) } }, + NotificationDebugItem(R.string.grade_summary_descriptive) { n -> + withStudent { + newGradeNotification.notifyDescriptive( + debugGradeDescriptiveItems.take(n), + it + ) + } + }, NotificationDebugItem(R.string.homework_title) { n -> withStudent { newHomeworkNotification.notify(debugHomeworkItems.take(n), it) } }, diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/mock/gradeDescriptive.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/mock/gradeDescriptive.kt new file mode 100644 index 00000000..d5a0c089 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/mock/gradeDescriptive.kt @@ -0,0 +1,48 @@ +package io.github.wulkanowy.ui.modules.debug.notification.mock + +import io.github.wulkanowy.data.db.entities.GradeDescriptive + +val debugGradeDescriptiveItems = listOf( + generateGradeDescriptive( + "Matematyka", + "Lorem ipsum dolor sit amet, consectetur adipiscing elit." + ), + generateGradeDescriptive("Fizyka", "Lorem ipsum dolor sit amet, consectetur adipiscing elit."), + generateGradeDescriptive( + "Geografia", + "Lorem ipsum dolor sit amet, consectetur adipiscing elit." + ), + generateGradeDescriptive( + "Sieci komputerowe", + "Lorem ipsum dolor sit amet, consectetur adipiscing elit." + ), + generateGradeDescriptive( + "Systemy operacyjne", + "Lorem ipsum dolor sit amet, consectetur adipiscing elit." + ), + generateGradeDescriptive( + "Język polski", + "Lorem ipsum dolor sit amet, consectetur adipiscing elit." + ), + generateGradeDescriptive( + "Język angielski", + "Lorem ipsum dolor sit amet, consectetur adipiscing elit." + ), + generateGradeDescriptive("Religia", "Lorem ipsum dolor sit amet, consectetur adipiscing elit."), + generateGradeDescriptive( + "Język niemiecki", + "Lorem ipsum dolor sit amet, consectetur adipiscing elit." + ), + generateGradeDescriptive( + "Wychowanie fizyczne", + "Lorem ipsum dolor sit amet, consectetur adipiscing elit." + ), +) + +private fun generateGradeDescriptive(subject: String, description: String) = + GradeDescriptive( + semesterId = 0, + studentId = 0, + subject = subject, + description = description + ) 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 ec4bd8e8..e8a5fa25 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 @@ -1,15 +1,23 @@ package io.github.wulkanowy.ui.modules.grade -import io.github.wulkanowy.data.* +import io.github.wulkanowy.data.Resource +import io.github.wulkanowy.data.dataOrNull import io.github.wulkanowy.data.db.entities.Grade +import io.github.wulkanowy.data.db.entities.GradeDescriptive import io.github.wulkanowy.data.db.entities.GradeSummary import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student +import io.github.wulkanowy.data.errorOrNull +import io.github.wulkanowy.data.flatResourceFlow +import io.github.wulkanowy.data.mapData +import io.github.wulkanowy.data.mapResourceData import io.github.wulkanowy.data.repositories.GradeRepository import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.SemesterRepository import io.github.wulkanowy.sdk.Sdk -import io.github.wulkanowy.ui.modules.grade.GradeAverageMode.* +import io.github.wulkanowy.ui.modules.grade.GradeAverageMode.ALL_YEAR +import io.github.wulkanowy.ui.modules.grade.GradeAverageMode.BOTH_SEMESTERS +import io.github.wulkanowy.ui.modules.grade.GradeAverageMode.ONE_SEMESTER import io.github.wulkanowy.utils.calcAverage import io.github.wulkanowy.utils.changeModifier import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -62,6 +70,7 @@ class GradeAverageProvider @Inject constructor( forceRefresh = forceRefresh, params = params, ) + BOTH_SEMESTERS -> calculateCombinedAverage( student = student, semesters = semesters, @@ -69,6 +78,7 @@ class GradeAverageProvider @Inject constructor( forceRefresh = forceRefresh, config = params, ) + ALL_YEAR -> calculateCombinedAverage( student = student, semesters = semesters, @@ -189,36 +199,73 @@ class GradeAverageProvider @Inject constructor( ): Flow>> { return gradeRepository.getGrades(student, semester, forceRefresh = forceRefresh) .mapResourceData { res -> - val (details, summaries) = res + val (details, summaries, descriptives) = res val isAnyAverage = summaries.any { it.average != .0 } val allGrades = details.groupBy { it.subject } + val descriptiveGradesBySubject = descriptives.associateBy { it.subject } - val items = summaries.emulateEmptySummaries( - student = student, - semester = semester, - grades = allGrades.toList(), - calcAverage = isAnyAverage, - params = params, - ).map { summary -> - val grades = allGrades[summary.subject].orEmpty() - GradeSubject( - subject = summary.subject, - average = if (!isAnyAverage || params.forceAverageCalc) { - grades.updateModifiers(student, params) - .calcAverage(params.isOptionalArithmeticAverage) - } else summary.average, - points = summary.pointsSum, - summary = summary, - grades = grades, - isVulcanAverage = isAnyAverage + val items = summaries + .createEmptySummariesByGradesIfNeeded( + student = student, + semester = semester, + grades = allGrades.toList(), + calcAverage = isAnyAverage, + params = params, ) - } + .createEmptySummariesByDescriptiveGradesIfNeeded( + student = student, + semester = semester, + descriptives = descriptives, + ) + .map { summary -> + val grades = allGrades[summary.subject].orEmpty() + val descriptiveGrade = descriptiveGradesBySubject[summary.subject] + + GradeSubject( + subject = summary.subject, + average = if (!isAnyAverage || params.forceAverageCalc) { + grades.updateModifiers(student, params) + .calcAverage(params.isOptionalArithmeticAverage) + } else summary.average, + points = summary.pointsSum, + summary = summary, + grades = grades, + descriptive = descriptiveGrade, + isVulcanAverage = isAnyAverage + ) + } items } } - private fun List.emulateEmptySummaries( + private fun List.createEmptySummariesByDescriptiveGradesIfNeeded( + student: Student, + semester: Semester, + descriptives: List + ): List { + val summarySubjects = this.map { it.subject } + val gradeSummaryToAdd = descriptives.mapNotNull { gradeDescriptive -> + if (gradeDescriptive.subject in summarySubjects) return@mapNotNull null + + GradeSummary( + studentId = student.studentId, + semesterId = semester.semesterId, + position = 0, + subject = gradeDescriptive.subject, + predictedGrade = "", + finalGrade = "", + proposedPoints = "", + finalPoints = "", + pointsSum = "", + average = .0 + ) + } + + return this + gradeSummaryToAdd + } + + private fun List.createEmptySummariesByGradesIfNeeded( student: Student, semester: Semester, grades: List>>, diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeSubject.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeSubject.kt index 57be55ee..a465551f 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeSubject.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeSubject.kt @@ -1,6 +1,7 @@ package io.github.wulkanowy.ui.modules.grade import io.github.wulkanowy.data.db.entities.Grade +import io.github.wulkanowy.data.db.entities.GradeDescriptive import io.github.wulkanowy.data.db.entities.GradeSummary data class GradeSubject( @@ -8,6 +9,7 @@ data class GradeSubject( val average: Double, val points: String, val summary: GradeSummary, + val descriptive: GradeDescriptive?, val grades: List, val isVulcanAverage: Boolean ) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsPresenter.kt index 4261c507..d9621f51 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsPresenter.kt @@ -1,13 +1,22 @@ package io.github.wulkanowy.ui.modules.grade.details -import io.github.wulkanowy.data.* import io.github.wulkanowy.data.db.entities.Grade import io.github.wulkanowy.data.enums.GradeExpandMode -import io.github.wulkanowy.data.enums.GradeSortingMode.* +import io.github.wulkanowy.data.enums.GradeSortingMode.ALPHABETIC +import io.github.wulkanowy.data.enums.GradeSortingMode.AVERAGE +import io.github.wulkanowy.data.enums.GradeSortingMode.DATE +import io.github.wulkanowy.data.flatResourceFlow +import io.github.wulkanowy.data.logResourceStatus +import io.github.wulkanowy.data.onResourceData +import io.github.wulkanowy.data.onResourceError +import io.github.wulkanowy.data.onResourceIntermediate +import io.github.wulkanowy.data.onResourceNotLoading +import io.github.wulkanowy.data.onResourceSuccess import io.github.wulkanowy.data.repositories.GradeRepository import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.SemesterRepository import io.github.wulkanowy.data.repositories.StudentRepository +import io.github.wulkanowy.data.resourceFlow import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.ui.modules.grade.GradeAverageProvider @@ -207,20 +216,20 @@ class GradeDetailsPresenter @Inject constructor( AVERAGE -> gradeSubjects.sortedByDescending { it.average } } } - .map { (subject, average, points, _, grades) -> - val subItems = grades + .map { gradeSubject -> + val subItems = gradeSubject.grades .sortedByDescending { it.date } .map { GradeDetailsItem(it, ViewType.ITEM) } val gradeDetailsItems = listOf( GradeDetailsItem( GradeDetailsHeader( - subject = subject, - average = average, - pointsSum = points, + subject = gradeSubject.subject, + average = gradeSubject.average, + pointsSum = gradeSubject.points, grades = subItems ).apply { - newGrades = grades.filter { grade -> !grade.isRead }.size + newGrades = gradeSubject.grades.filter { grade -> !grade.isRead }.size }, ViewType.HEADER ) ) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryAdapter.kt index 8dcade56..95cf97be 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryAdapter.kt @@ -2,16 +2,16 @@ package io.github.wulkanowy.ui.modules.grade.summary import android.annotation.SuppressLint import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup +import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView import io.github.wulkanowy.R -import io.github.wulkanowy.data.db.entities.GradeSummary import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.databinding.ItemGradeSummaryBinding import io.github.wulkanowy.databinding.ScrollableHeaderGradeSummaryBinding import io.github.wulkanowy.sdk.scrapper.grades.isGradeValid import io.github.wulkanowy.utils.calcFinalAverage +import io.github.wulkanowy.utils.ifNullOrBlank import java.util.Locale import javax.inject.Inject @@ -24,7 +24,7 @@ class GradeSummaryAdapter @Inject constructor( ITEM(2) } - var items = emptyList() + var items = emptyList() var onCalculatedHelpClickListener: () -> Unit = {} @@ -44,9 +44,11 @@ class GradeSummaryAdapter @Inject constructor( ViewType.HEADER.id -> HeaderViewHolder( ScrollableHeaderGradeSummaryBinding.inflate(inflater, parent, false) ) + ViewType.ITEM.id -> ItemViewHolder( ItemGradeSummaryBinding.inflate(inflater, parent, false) ) + else -> throw IllegalStateException() } } @@ -60,19 +62,23 @@ class GradeSummaryAdapter @Inject constructor( private fun bindHeaderViewHolder(binding: ScrollableHeaderGradeSummaryBinding) { if (items.isEmpty()) return + val gradeSummaries = items + .filter { it.gradeDescriptive == null } + .map { it.gradeSummary } val context = binding.root.context - val finalItemsCount = items.count { isGradeValid(it.finalGrade) } - val calculatedItemsCount = items.count { value -> value.average != 0.0 } - val allItemsCount = items.count { !it.subject.equals("zachowanie", true) } - val finalAverage = items.calcFinalAverage( + val finalItemsCount = gradeSummaries.count { isGradeValid(it.finalGrade) } + val calculatedItemsCount = gradeSummaries.count { value -> value.average != 0.0 } + val allItemsCount = gradeSummaries.count { !it.subject.equals("zachowanie", true) } + val finalAverage = gradeSummaries.calcFinalAverage( preferencesRepository.gradePlusModifier, preferencesRepository.gradeMinusModifier ) - val calculatedAverage = items.filter { value -> value.average != 0.0 } + val calculatedAverage = gradeSummaries.filter { value -> value.average != 0.0 } .map { values -> values.average } .reversed() // fix average precision .average() + .let { if (it.isNaN()) 0.0 else it } with(binding) { gradeSummaryScrollableHeaderFinal.text = formatAverage(finalAverage) @@ -95,16 +101,28 @@ class GradeSummaryAdapter @Inject constructor( } @SuppressLint("SetTextI18n") - private fun bindItemViewHolder(binding: ItemGradeSummaryBinding, item: GradeSummary) { - with(binding) { - gradeSummaryItemTitle.text = item.subject - gradeSummaryItemPoints.text = item.pointsSum - gradeSummaryItemAverage.text = formatAverage(item.average, "") - gradeSummaryItemPredicted.text = "${item.predictedGrade} ${item.proposedPoints}".trim() - gradeSummaryItemFinal.text = "${item.finalGrade} ${item.finalPoints}".trim() + private fun bindItemViewHolder(binding: ItemGradeSummaryBinding, item: GradeSummaryItem) { + val (gradeSummary, gradeDescriptive) = item - gradeSummaryItemPointsContainer.visibility = - if (item.pointsSum.isBlank()) View.GONE else View.VISIBLE + with(binding) { + gradeSummaryItemTitle.text = gradeSummary.subject + gradeSummaryItemPoints.text = gradeSummary.pointsSum + gradeSummaryItemAverage.text = formatAverage(gradeSummary.average, "") + gradeSummaryItemPredicted.text = + "${gradeSummary.predictedGrade} ${gradeSummary.proposedPoints}".trim() + gradeSummaryItemFinal.text = + "${gradeSummary.finalGrade} ${gradeSummary.finalPoints}".trim() + gradeSummaryItemDescriptive.text = gradeDescriptive?.description.ifNullOrBlank { + root.context.getString(R.string.all_no_data) + } + + gradeSummaryItemFinalDivider.isVisible = gradeDescriptive == null + gradeSummaryItemPredictedDivider.isVisible = gradeDescriptive == null + gradeSummaryItemPointsDivider.isVisible = gradeDescriptive == null + gradeSummaryItemPredictedContainer.isVisible = gradeDescriptive == null + gradeSummaryItemFinalContainer.isVisible = gradeDescriptive == null + gradeSummaryItemDescriptiveContainer.isVisible = gradeDescriptive != null + gradeSummaryItemPointsContainer.isVisible = gradeSummary.pointsSum.isNotBlank() } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryFragment.kt index abd0b13c..35b2edd5 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryFragment.kt @@ -5,12 +5,10 @@ import android.view.View import android.view.View.GONE import android.view.View.INVISIBLE import android.view.View.VISIBLE -import androidx.appcompat.app.AlertDialog import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R -import io.github.wulkanowy.data.db.entities.GradeSummary import io.github.wulkanowy.databinding.FragmentGradeSummaryBinding import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.modules.grade.GradeFragment @@ -72,7 +70,7 @@ class GradeSummaryFragment : } } - override fun updateData(data: List) { + override fun updateData(data: List) { with(gradeSummaryAdapter) { items = data notifyDataSetChanged() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryItem.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryItem.kt new file mode 100644 index 00000000..cf0f1d92 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryItem.kt @@ -0,0 +1,9 @@ +package io.github.wulkanowy.ui.modules.grade.summary + +import io.github.wulkanowy.data.db.entities.GradeDescriptive +import io.github.wulkanowy.data.db.entities.GradeSummary + +data class GradeSummaryItem( + val gradeSummary: GradeSummary, + val gradeDescriptive: GradeDescriptive? +) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryPresenter.kt index 32508ff6..d762df02 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryPresenter.kt @@ -1,9 +1,16 @@ package io.github.wulkanowy.ui.modules.grade.summary -import io.github.wulkanowy.data.* -import io.github.wulkanowy.data.db.entities.GradeSummary -import io.github.wulkanowy.data.enums.GradeSortingMode -import io.github.wulkanowy.data.enums.GradeSortingMode.* +import io.github.wulkanowy.data.enums.GradeSortingMode.ALPHABETIC +import io.github.wulkanowy.data.enums.GradeSortingMode.AVERAGE +import io.github.wulkanowy.data.enums.GradeSortingMode.DATE +import io.github.wulkanowy.data.flatResourceFlow +import io.github.wulkanowy.data.logResourceStatus +import io.github.wulkanowy.data.mapResourceData +import io.github.wulkanowy.data.onResourceData +import io.github.wulkanowy.data.onResourceError +import io.github.wulkanowy.data.onResourceIntermediate +import io.github.wulkanowy.data.onResourceNotLoading +import io.github.wulkanowy.data.onResourceSuccess import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.ui.base.BasePresenter @@ -128,7 +135,7 @@ class GradeSummaryPresenter @Inject constructor( view?.showFinalAverageHelpDialog() } - private fun createGradeSummaryItems(items: List): List { + private fun createGradeSummaryItems(items: List): List { return items .filter { !checkEmpty(it) } .let { gradeSubjects -> @@ -136,21 +143,32 @@ class GradeSummaryPresenter @Inject constructor( DATE -> gradeSubjects.sortedByDescending { gradeDetailsWithAverage -> gradeDetailsWithAverage.grades.maxByOrNull { it.date }?.date } + ALPHABETIC -> gradeSubjects.sortedBy { gradeDetailsWithAverage -> gradeDetailsWithAverage.subject.lowercase() } + AVERAGE -> gradeSubjects.sortedByDescending { it.average } } } - .map { it.summary.copy(average = it.average) } + .map { + val gradeSummary = it.summary.copy(average = it.average) + val descriptive = it.descriptive + GradeSummaryItem( + gradeSummary = gradeSummary, + gradeDescriptive = descriptive, + ) + } + } private fun checkEmpty(gradeSummary: GradeSubject): Boolean { return gradeSummary.run { summary.finalGrade.isBlank() - && summary.predictedGrade.isBlank() - && average == .0 - && points.isBlank() + && summary.predictedGrade.isBlank() + && average == .0 + && points.isBlank() + && descriptive == null } } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryView.kt index 156731c3..36bd6142 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryView.kt @@ -1,6 +1,5 @@ package io.github.wulkanowy.ui.modules.grade.summary -import io.github.wulkanowy.data.db.entities.GradeSummary import io.github.wulkanowy.ui.base.BaseView interface GradeSummaryView : BaseView { @@ -13,7 +12,7 @@ interface GradeSummaryView : BaseView { fun initView() - fun updateData(data: List) + fun updateData(data: List) fun resetView() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginData.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginData.kt index 2c11bb6d..b066cceb 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginData.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginData.kt @@ -7,5 +7,6 @@ data class LoginData( val password: String, val baseUrl: String, val domainSuffix: String, - val symbol: String?, + val defaultSymbol: String, + val userEnteredSymbol: String? = null, ) : Serializable diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedPresenter.kt index a17ad003..009f26e1 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedPresenter.kt @@ -71,7 +71,7 @@ class LoginAdvancedPresenter @Inject constructor( fun updateUsernameLabel() { view?.apply { - setUsernameLabel(if ("vulcan" in formHostValue || "fakelog" in formHostValue) emailLabel else nicknameLabel) + setUsernameLabel(if ("vulcan" in formHostValue || "wulkanowy" in formHostValue) emailLabel else nicknameLabel) } } @@ -79,7 +79,7 @@ class LoginAdvancedPresenter @Inject constructor( view?.apply { clearPassError() clearUsernameError() - if (formHostValue.contains("fakelog")) { + if (formHostValue.contains("wulkanowy")) { setDefaultCredentials( "jan@fakelog.cf", "jan123", "powiatwulkanowy", "FK100000", "999999" ) @@ -155,7 +155,7 @@ class LoginAdvancedPresenter @Inject constructor( password = view?.formPassValue.orEmpty().trim(), baseUrl = view?.formHostValue.orEmpty().trim(), domainSuffix = view?.formDomainSuffix.orEmpty().trim(), - symbol = view?.formSymbolValue.orEmpty().trim().getNormalizedSymbol(), + defaultSymbol = view?.formSymbolValue.orEmpty().trim().getNormalizedSymbol(), ) when (it.data.symbols.size) { 0 -> view?.navigateToSymbol(loginData) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt index 8e9b86fa..1c492069 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt @@ -7,6 +7,7 @@ import android.view.View.GONE import android.view.View.VISIBLE import androidx.core.view.isVisible import androidx.core.widget.doOnTextChanged +import androidx.fragment.app.setFragmentResultListener import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.AdminMessage @@ -14,6 +15,7 @@ import io.github.wulkanowy.data.pojos.RegisterUser import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.databinding.FragmentLoginFormBinding import io.github.wulkanowy.ui.base.BaseFragment +import io.github.wulkanowy.ui.modules.captcha.CaptchaDialog import io.github.wulkanowy.ui.modules.dashboard.viewholders.AdminMessageViewHolder import io.github.wulkanowy.ui.modules.login.LoginActivity import io.github.wulkanowy.ui.modules.login.LoginData @@ -72,6 +74,13 @@ class LoginFormFragment : BaseFragment(R.layout.fragme super.onViewCreated(view, savedInstanceState) binding = FragmentLoginFormBinding.bind(view) presenter.onAttachView(this) + initializeCaptchaResultObserver() + } + + private fun initializeCaptchaResultObserver() { + setFragmentResultListener(CaptchaDialog.CAPTCHA_SUCCESS) { _, _ -> + presenter.onRetryAfterCaptcha() + } } override fun initView() { @@ -85,6 +94,7 @@ class LoginFormFragment : BaseFragment(R.layout.fragme loginFormUsername.doOnTextChanged { _, _, _, _ -> presenter.onUsernameTextChanged() } loginFormPass.doOnTextChanged { _, _, _, _ -> presenter.onPassTextChanged() } loginFormHost.setOnItemClickListener { _, _, _, _ -> presenter.onHostSelected() } + loginFormDomainSuffix.doOnTextChanged { _, _, _, _ -> presenter.onDomainSuffixChanged() } loginFormSignIn.setOnClickListener { presenter.onSignInClick() } loginFormAdvancedButton.setOnClickListener { presenter.onAdvancedLoginClick() } loginFormPrivacyLink.setOnClickListener { presenter.onPrivacyLinkClick() } @@ -179,6 +189,12 @@ class LoginFormFragment : BaseFragment(R.layout.fragme } } + override fun setDomainSuffixInvalid() { + with(binding.loginFormDomainSuffixLayout) { + error = getString(R.string.login_invalid_domain_suffix) + } + } + override fun clearUsernameError() { binding.loginFormUsernameLayout.error = null binding.loginFormErrorBox.isVisible = false @@ -197,6 +213,10 @@ class LoginFormFragment : BaseFragment(R.layout.fragme binding.loginFormErrorBox.isVisible = false } + override fun clearDomainSuffixError() { + binding.loginFormDomainSuffixLayout.error = null + } + override fun showSoftKeyboard() { activity?.showSoftInput() } 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 ad535c38..39bc3f02 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 @@ -90,7 +91,7 @@ class LoginFormPresenter @Inject constructor( clearPassError() clearUsernameError() clearHostError() - if (formHostValue.contains("fakelog")) { + if (formHostValue.contains("wulkanowy")) { setCredentials("jan@fakelog.cf", "jan123") } else if (formUsernameValue == "jan@fakelog.cf" && formPassValue == "jan123") { setCredentials("", "") @@ -101,6 +102,12 @@ class LoginFormPresenter @Inject constructor( } } + fun onDomainSuffixChanged() { + view?.apply { + clearDomainSuffixError() + } + } + fun updateCustomDomainSuffixVisibility() { view?.run { showDomainSuffixInput("customSuffix" in formHostValue) @@ -148,14 +155,18 @@ class LoginFormPresenter @Inject constructor( password = password, baseUrl = host, domainSuffix = domainSuffix, - symbol = symbol + defaultSymbol = symbol ) } + fun onRetryAfterCaptcha() { + onSignInClick() + } + fun onSignInClick() { val loginData = getLoginData() - if (!validateCredentials(loginData.login, loginData.password, loginData.baseUrl)) return + if (!validateCredentials(loginData)) return resourceFlow { studentRepository.getUserSubjectsFromScrapper( @@ -163,7 +174,7 @@ class LoginFormPresenter @Inject constructor( password = loginData.password, scrapperBaseUrl = loginData.baseUrl, domainSuffix = loginData.domainSuffix, - symbol = loginData.symbol.orEmpty(), + symbol = loginData.defaultSymbol, ) } .logResourceStatus("login") @@ -194,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( @@ -225,24 +239,29 @@ class LoginFormPresenter @Inject constructor( view?.onRecoverClick() } - private fun validateCredentials(login: String, password: String, host: String): Boolean { + private fun validateCredentials(loginData: LoginData): Boolean { var isCorrect = true - if (login.isEmpty()) { + if (loginData.login.isEmpty()) { view?.setErrorUsernameRequired() isCorrect = false } else { - if ("@" in login && "login" in host) { + if ("@" in loginData.login && "login" in loginData.baseUrl) { view?.setErrorLoginRequired() isCorrect = false } - if ("@" !in login && "email" in host) { + if ("@" !in loginData.login && "email" in loginData.baseUrl) { view?.setErrorEmailRequired() isCorrect = false } - if ("@" in login && "||" !in login && "login" !in host && "email" !in host) { - val emailHost = login.substringAfter("@") - val emailDomain = URL(host).host + + val isEmailLogin = "@" in loginData.login + val isEmailWithLogin = "||" !in loginData.login + val isLoginNotRequired = "login" !in loginData.baseUrl + val isEmailNotRequired = "email" !in loginData.baseUrl + if (isEmailLogin && isEmailWithLogin && isLoginNotRequired && isEmailNotRequired) { + val emailHost = loginData.login.substringAfter("@") + val emailDomain = URL(loginData.baseUrl).host if (!emailHost.equals(emailDomain, true)) { view?.setErrorEmailInvalid(domain = emailDomain) isCorrect = false @@ -250,16 +269,21 @@ class LoginFormPresenter @Inject constructor( } } - if (password.isEmpty()) { + if (loginData.password.isEmpty()) { view?.setErrorPassRequired(focus = isCorrect) isCorrect = false } - if (password.length < 6 && password.isNotEmpty()) { + if (loginData.password.length < 6 && loginData.password.isNotEmpty()) { view?.setErrorPassInvalid(focus = isCorrect) isCorrect = false } + if (loginData.domainSuffix !in listOf("", "rc", "kurs")) { + view?.setDomainSuffixInvalid() + isCorrect = false + } + return isCorrect } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormView.kt index f2b7b100..6ea22d18 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormView.kt @@ -46,12 +46,16 @@ interface LoginFormView : BaseView { fun setErrorEmailInvalid(domain: String) + fun setDomainSuffixInvalid() + fun clearUsernameError() fun clearPassError() fun clearHostError() + fun clearDomainSuffixError() + fun showSoftKeyboard() fun hideSoftKeyboard() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/recover/LoginRecoverPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/recover/LoginRecoverPresenter.kt index a424df40..18902e01 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/recover/LoginRecoverPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/recover/LoginRecoverPresenter.kt @@ -38,7 +38,7 @@ class LoginRecoverPresenter @Inject constructor( fun onHostSelected() { view?.run { - if ("fakelog" in recoverHostValue) setDefaultCredentials("jan@fakelog.cf") + if ("wulkanowy" in recoverHostValue) setDefaultCredentials("jan@fakelog.cf") clearUsernameError() updateFields() } @@ -60,7 +60,7 @@ class LoginRecoverPresenter @Inject constructor( resourceFlow { recoverRepository.getReCaptchaSiteKey( host, - symbol.ifBlank { "Default" }) + symbol.ifBlank { "default" }) }.onEach { when (it) { is Resource.Loading -> view?.run { @@ -103,7 +103,7 @@ class LoginRecoverPresenter @Inject constructor( fun onReCaptchaVerified(reCaptchaResponse: String) { val username = view?.recoverNameValue.orEmpty() val host = view?.recoverHostValue.orEmpty() - val symbol = view?.formHostSymbol.ifNullOrBlank { "Default" } + val symbol = view?.formHostSymbol.ifNullOrBlank { "default" } resourceFlow { recoverRepository.sendRecoverRequest( diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectFragment.kt index 06efd8d9..0fe36aa9 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectFragment.kt @@ -10,13 +10,11 @@ import io.github.wulkanowy.data.pojos.RegisterUser import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.databinding.FragmentLoginStudentSelectBinding import io.github.wulkanowy.ui.base.BaseFragment -import io.github.wulkanowy.ui.modules.auth.AuthDialog import io.github.wulkanowy.ui.modules.login.LoginActivity import io.github.wulkanowy.ui.modules.login.LoginData import io.github.wulkanowy.ui.modules.login.support.LoginSupportDialog import io.github.wulkanowy.ui.modules.login.support.LoginSupportInfo import io.github.wulkanowy.utils.AppInfo -import io.github.wulkanowy.utils.openEmailClient import io.github.wulkanowy.utils.openInternetBrowser import io.github.wulkanowy.utils.serializable import javax.inject.Inject diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenter.kt index 6cbdfbb8..34441418 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenter.kt @@ -111,8 +111,8 @@ class LoginStudentSelectPresenter @Inject constructor( val notEmptySymbols = registerUser.symbols.filter { it.schools.isNotEmpty() } val emptySymbols = registerUser.symbols.filter { it.schools.isEmpty() } - if (emptySymbols.isNotEmpty() && notEmptySymbols.isNotEmpty() && emptySymbols.any { it.symbol == loginData.symbol }) { - add(createEmptySymbolItem(emptySymbols.first { it.symbol == loginData.symbol })) + if (emptySymbols.isNotEmpty() && notEmptySymbols.isNotEmpty() && emptySymbols.any { it.symbol == loginData.userEnteredSymbol }) { + add(createEmptySymbolItem(emptySymbols.first { it.symbol == loginData.userEnteredSymbol })) } addAll(createNotEmptySymbolItems(notEmptySymbols, students)) @@ -317,7 +317,7 @@ class LoginStudentSelectPresenter @Inject constructor( loginData = loginData, registerUser = registerUser, lastErrorMessage = lastError?.message, - enteredSymbol = loginData.symbol, + enteredSymbol = loginData.userEnteredSymbol, ) ) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/support/LoginSupportDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/support/LoginSupportDialog.kt index fcf7f51c..4be2dbaa 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/support/LoginSupportDialog.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/support/LoginSupportDialog.kt @@ -105,7 +105,7 @@ class LoginSupportDialog : BaseDialogFragment() { "${appInfo.systemManufacturer} ${appInfo.systemModel}", appInfo.systemVersion.toString(), "${appInfo.versionName}-${appInfo.buildFlavor}", - supportInfo.loginData.baseUrl + "/" + supportInfo.loginData.symbol, + supportInfo.loginData.let { "${it.baseUrl}/${it.defaultSymbol}/${it.userEnteredSymbol}" }, preferencesRepository.installationId, getLastErrorFromStudentSelectScreen(), dialogLoginSupportSchoolInput.text.takeIf { !it.isNullOrBlank() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolPresenter.kt index 02bfde5d..5c31f14d 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolPresenter.kt @@ -60,7 +60,7 @@ class LoginSymbolPresenter @Inject constructor( } loginData = loginData.copy( - symbol = view?.symbolValue?.getNormalizedSymbol(), + userEnteredSymbol = view?.symbolValue?.getNormalizedSymbol(), ) resourceFlow { studentRepository.getUserSubjectsFromScrapper( @@ -68,7 +68,7 @@ class LoginSymbolPresenter @Inject constructor( password = loginData.password, scrapperBaseUrl = loginData.baseUrl, domainSuffix = loginData.domainSuffix, - symbol = loginData.symbol.orEmpty(), + symbol = loginData.userEnteredSymbol.orEmpty(), ) }.onEach { user -> registerUser = user.dataOrNull @@ -93,13 +93,10 @@ class LoginSymbolPresenter @Inject constructor( else -> { val enteredSymbolDetails = user.data.symbols .firstOrNull() - ?.takeIf { it.symbol == loginData.symbol } + ?.takeIf { it.symbol == loginData.userEnteredSymbol } if (enteredSymbolDetails?.error is InvalidSymbolException) { - view?.run { - setErrorSymbolInvalid() - showContact(true) - } + showInvalidSymbolError() } else { Timber.i("Login with symbol result: Success") view?.navigateToStudentSelect(loginData, requireNotNull(user.data)) @@ -128,6 +125,9 @@ class LoginSymbolPresenter @Inject constructor( loginErrorHandler.dispatch(user.error) lastError = user.error view?.showContact(true) + if (user.error is InvalidSymbolException) { + showInvalidSymbolError() + } } } }.onResourceNotLoading { @@ -145,6 +145,13 @@ class LoginSymbolPresenter @Inject constructor( return normalizedSymbol in definitelyInvalidSymbols } + private fun showInvalidSymbolError() { + view?.run { + setErrorSymbolInvalid() + showContact(true) + } + } + fun onFaqClick() { view?.openFaqPage() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetProvider.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetProvider.kt index bafb2d7e..1ab079a3 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetProvider.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetProvider.kt @@ -11,10 +11,8 @@ import android.view.View import android.widget.RemoteViews import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R -import io.github.wulkanowy.data.Resource -import io.github.wulkanowy.data.dataOrNull +import io.github.wulkanowy.data.dataOrThrow import io.github.wulkanowy.data.db.SharedPrefProvider -import io.github.wulkanowy.data.db.entities.LuckyNumber import io.github.wulkanowy.data.repositories.LuckyNumberRepository import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.toFirstResult @@ -69,8 +67,7 @@ class LuckyNumberWidgetProvider : AppWidgetProvider() { appWidgetIds?.forEach { widgetId -> val studentId = sharedPref.getLong(getStudentWidgetKey(widgetId), 0) - val luckyNumberResource = getLuckyNumber(studentId, widgetId) - val luckyNumber = luckyNumberResource.dataOrNull?.luckyNumber?.toString() + val luckyNumber = getLuckyNumber(studentId, widgetId)?.luckyNumber?.toString() val remoteView = RemoteViews(context.packageName, R.layout.widget_luckynumber) .apply { setTextViewText(R.id.luckyNumberWidgetValue, luckyNumber ?: "-") @@ -143,18 +140,18 @@ class LuckyNumberWidgetProvider : AppWidgetProvider() { sharedPref.putLong(getStudentWidgetKey(appWidgetId), it.id) } } + else -> null } if (currentStudent != null) { luckyNumberRepository.getLuckyNumber(currentStudent, forceRefresh = false) .toFirstResult() - } else { - Resource.Success(null) - } + .dataOrThrow + } else null } catch (e: Exception) { Timber.e(e, "An error has occurred in lucky number provider") - Resource.Error(e) + null } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt index ba0ef405..62c16257 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt @@ -16,6 +16,7 @@ import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams import androidx.fragment.app.DialogFragment import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import com.google.android.material.dialog.MaterialAlertDialogBuilder @@ -30,6 +31,8 @@ import io.github.wulkanowy.databinding.ActivityMainBinding import io.github.wulkanowy.ui.base.BaseActivity import io.github.wulkanowy.ui.modules.Destination import io.github.wulkanowy.ui.modules.account.accountquick.AccountQuickDialog +import io.github.wulkanowy.ui.modules.auth.AuthDialog +import io.github.wulkanowy.ui.modules.captcha.CaptchaDialog import io.github.wulkanowy.ui.modules.settings.appearance.menuorder.AppMenuItem import io.github.wulkanowy.utils.AnalyticsHelper import io.github.wulkanowy.utils.AppInfo @@ -40,10 +43,17 @@ import io.github.wulkanowy.utils.dpToPx import io.github.wulkanowy.utils.nickOrName import io.github.wulkanowy.utils.safelyPopFragments import io.github.wulkanowy.utils.setOnViewChangeListener +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import timber.log.Timber import javax.inject.Inject +import kotlin.time.Duration.Companion.seconds @AndroidEntryPoint class MainActivity : BaseActivity(), MainView, @@ -73,6 +83,8 @@ class MainActivity : BaseActivity(), MainVie private val navController = FragNavController(supportFragmentManager, R.id.main_fragment_container) + private val captchaVerificationEvent = MutableSharedFlow() + companion object { private const val EXTRA_START_DESTINATION = "start_destination_json" @@ -144,6 +156,7 @@ class MainActivity : BaseActivity(), MainVie initializeToolbar() initializeBottomNavigation(startMenuIndex, rootAppMenuItems) initializeNavController(startMenuIndex, rootUpdatedDestinations) + initializeCaptchaVerificationEvent() } private fun initializeNavController( @@ -323,6 +336,27 @@ class MainActivity : BaseActivity(), MainVie .show() } + @OptIn(FlowPreview::class) + private fun initializeCaptchaVerificationEvent() { + captchaVerificationEvent + .debounce(1.seconds) + .onEach { url -> + Timber.d("Showing captcha dialog for: $url") + showDialogFragment(CaptchaDialog.newInstance(url)) + } + .launchIn(lifecycleScope) + } + + override fun onCaptchaVerificationRequired(url: String?) { + lifecycleScope.launch { + captchaVerificationEvent.emit(url) + } + } + + override fun showAuthDialog() { + showDialogFragment(AuthDialog.newInstance()) + } + override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) navController.onSaveInstanceState(outState) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsFragment.kt index 21f56498..f8d1323c 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsFragment.kt @@ -3,6 +3,7 @@ package io.github.wulkanowy.ui.modules.settings import android.os.Bundle import androidx.preference.PreferenceFragmentCompat import io.github.wulkanowy.R +import io.github.wulkanowy.ui.base.BaseActivity import io.github.wulkanowy.ui.modules.main.MainView import timber.log.Timber @@ -24,7 +25,11 @@ class SettingsFragment : PreferenceFragmentCompat(), MainView.TitledView, Settin override fun showMessage(text: String) {} - override fun showExpiredDialog() {} + override fun showExpiredCredentialsDialog() {} + + override fun onCaptchaVerificationRequired(url: String?) = Unit + + override fun showDecryptionFailedDialog() {} override fun openClearLoginView() {} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/advanced/AdvancedFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/advanced/AdvancedFragment.kt index 1b8d1a8f..3ef1a80a 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/advanced/AdvancedFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/advanced/AdvancedFragment.kt @@ -8,7 +8,6 @@ import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.ui.base.BaseActivity import io.github.wulkanowy.ui.base.ErrorDialog -import io.github.wulkanowy.ui.modules.auth.AuthDialog import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.utils.AppInfo import javax.inject.Inject @@ -47,8 +46,16 @@ class AdvancedFragment : PreferenceFragmentCompat(), (activity as? BaseActivity<*, *>)?.showMessage(text) } - override fun showExpiredDialog() { - (activity as? BaseActivity<*, *>)?.showExpiredDialog() + override fun showExpiredCredentialsDialog() { + (activity as? BaseActivity<*, *>)?.showExpiredCredentialsDialog() + } + + override fun onCaptchaVerificationRequired(url: String?) { + (activity as? BaseActivity<*, *>)?.onCaptchaVerificationRequired(url) + } + + override fun showDecryptionFailedDialog() { + (activity as? BaseActivity<*, *>)?.showDecryptionFailedDialog() } override fun showChangePasswordSnackbar(redirectUrl: String) { @@ -64,7 +71,7 @@ class AdvancedFragment : PreferenceFragmentCompat(), } override fun showAuthDialog() { - AuthDialog.newInstance().show(childFragmentManager, "auth_dialog") + (activity as? BaseActivity<*, *>)?.showAuthDialog() } override fun onResume() { 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 70dd694c..3d0c8052 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 @@ -9,7 +9,6 @@ import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.ui.base.BaseActivity import io.github.wulkanowy.ui.base.ErrorDialog -import io.github.wulkanowy.ui.modules.auth.AuthDialog import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.utils.AppInfo import javax.inject.Inject @@ -63,8 +62,16 @@ class AppearanceFragment : PreferenceFragmentCompat(), (activity as? BaseActivity<*, *>)?.showMessage(text) } - override fun showExpiredDialog() { - (activity as? BaseActivity<*, *>)?.showExpiredDialog() + override fun showExpiredCredentialsDialog() { + (activity as? BaseActivity<*, *>)?.showExpiredCredentialsDialog() + } + + override fun onCaptchaVerificationRequired(url: String?) { + (activity as? BaseActivity<*, *>)?.onCaptchaVerificationRequired(url) + } + + override fun showDecryptionFailedDialog() { + (activity as? BaseActivity<*, *>)?.showDecryptionFailedDialog() } override fun showChangePasswordSnackbar(redirectUrl: String) { @@ -80,7 +87,7 @@ class AppearanceFragment : PreferenceFragmentCompat(), } override fun showAuthDialog() { - AuthDialog.newInstance().show(childFragmentManager, "auth_dialog") + (activity as? BaseActivity<*, *>)?.showAuthDialog() } override fun onResume() { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsFragment.kt index af4c4e6a..0bf9ddad 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsFragment.kt @@ -21,7 +21,6 @@ import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.ui.base.BaseActivity import io.github.wulkanowy.ui.base.ErrorDialog -import io.github.wulkanowy.ui.modules.auth.AuthDialog import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.openInternetBrowser @@ -133,8 +132,16 @@ class NotificationsFragment : PreferenceFragmentCompat(), (activity as? BaseActivity<*, *>)?.showMessage(text) } - override fun showExpiredDialog() { - (activity as? BaseActivity<*, *>)?.showExpiredDialog() + override fun showExpiredCredentialsDialog() { + (activity as? BaseActivity<*, *>)?.showExpiredCredentialsDialog() + } + + override fun onCaptchaVerificationRequired(url: String?) { + (activity as? BaseActivity<*, *>)?.onCaptchaVerificationRequired(url) + } + + override fun showDecryptionFailedDialog() { + (activity as? BaseActivity<*, *>)?.showDecryptionFailedDialog() } override fun showChangePasswordSnackbar(redirectUrl: String) { @@ -150,7 +157,7 @@ class NotificationsFragment : PreferenceFragmentCompat(), } override fun showAuthDialog() { - AuthDialog.newInstance().show(childFragmentManager, "auth_dialog") + (activity as? BaseActivity<*, *>)?.showAuthDialog() } override fun showFixSyncDialog() { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncFragment.kt index f48abe9b..d5714483 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncFragment.kt @@ -10,7 +10,6 @@ import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.ui.base.BaseActivity import io.github.wulkanowy.ui.base.ErrorDialog -import io.github.wulkanowy.ui.modules.auth.AuthDialog import io.github.wulkanowy.ui.modules.main.MainView import javax.inject.Inject @@ -84,8 +83,16 @@ class SyncFragment : PreferenceFragmentCompat(), } } - override fun showExpiredDialog() { - (activity as? BaseActivity<*, *>)?.showExpiredDialog() + override fun showExpiredCredentialsDialog() { + (activity as? BaseActivity<*, *>)?.showExpiredCredentialsDialog() + } + + override fun onCaptchaVerificationRequired(url: String?) { + (activity as? BaseActivity<*, *>)?.onCaptchaVerificationRequired(url) + } + + override fun showDecryptionFailedDialog() { + (activity as? BaseActivity<*, *>)?.showDecryptionFailedDialog() } override fun showChangePasswordSnackbar(redirectUrl: String) { @@ -101,7 +108,7 @@ class SyncFragment : PreferenceFragmentCompat(), } override fun showAuthDialog() { - AuthDialog.newInstance().show(childFragmentManager, "auth_dialog") + (activity as? BaseActivity<*, *>)?.showAuthDialog() } override fun onResume() { 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 6b442d1c..e83f2517 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 @@ -2,9 +2,7 @@ package io.github.wulkanowy.ui.modules.timetable import android.os.Handler import android.os.Looper -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.db.entities.Timetable import io.github.wulkanowy.data.enums.TimetableGapsMode.BETWEEN_AND_BEFORE_LESSONS import io.github.wulkanowy.data.enums.TimetableGapsMode.NO_GAPS @@ -20,8 +18,8 @@ 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.toFirstResult -import io.github.wulkanowy.data.waitForResult +import io.github.wulkanowy.domain.timetable.IsStudentHasLessonsOnWeekendUseCase +import io.github.wulkanowy.domain.timetable.IsWeekendHasLessonsUseCase import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.utils.AnalyticsHelper @@ -31,16 +29,12 @@ import io.github.wulkanowy.utils.isHolidays import io.github.wulkanowy.utils.isJustFinished import io.github.wulkanowy.utils.isShowTimeUntil import io.github.wulkanowy.utils.left -import io.github.wulkanowy.utils.monday import io.github.wulkanowy.utils.nextOrSameSchoolDay import io.github.wulkanowy.utils.nextSchoolDay import io.github.wulkanowy.utils.previousSchoolDay -import io.github.wulkanowy.utils.sunday import io.github.wulkanowy.utils.toFormattedString import io.github.wulkanowy.utils.until -import kotlinx.coroutines.flow.firstOrNull import timber.log.Timber -import java.time.DayOfWeek import java.time.Instant import java.time.LocalDate import java.time.LocalDate.now @@ -54,6 +48,8 @@ class TimetablePresenter @Inject constructor( errorHandler: ErrorHandler, studentRepository: StudentRepository, private val timetableRepository: TimetableRepository, + private val isStudentHasLessonsOnWeekendUseCase: IsStudentHasLessonsOnWeekendUseCase, + private val isWeekendHasLessonsUseCase: IsWeekendHasLessonsUseCase, private val semesterRepository: SemesterRepository, private val prefRepository: PreferencesRepository, private val analytics: AnalyticsHelper, @@ -153,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, @@ -165,7 +161,7 @@ class TimetablePresenter @Inject constructor( } .logResourceStatus("load timetable data") .onResourceData { - isWeekendHasLessons = isWeekendHasLessons || isWeekendHasLessons(it.lessons) + isWeekendHasLessons = isWeekendHasLessons || isWeekendHasLessonsUseCase(it.lessons) view?.run { enableSwipe(true) @@ -197,17 +193,9 @@ class TimetablePresenter @Inject constructor( .launch() } - private suspend fun checkInitialAndCurrentDate(student: Student, semester: Semester) { + private suspend fun checkInitialAndCurrentDate(semester: Semester) { if (initialDate == null) { - val lessons = timetableRepository.getTimetable( - student = student, - semester = semester, - start = now().monday, - end = now().sunday, - forceRefresh = false, - timetableType = TimetableRepository.TimetableType.NORMAL - ).toFirstResult().dataOrNull?.lessons.orEmpty() - isWeekendHasLessons = isWeekendHasLessons(lessons) + isWeekendHasLessons = isStudentHasLessonsOnWeekendUseCase(semester) initialDate = getInitialDate(semester) } @@ -216,15 +204,6 @@ class TimetablePresenter @Inject constructor( } } - private fun isWeekendHasLessons( - lessons: List, - ): Boolean = lessons.any { - it.date.dayOfWeek in listOf( - DayOfWeek.SATURDAY, - DayOfWeek.SUNDAY, - ) - } - private fun getInitialDate(semester: Semester): LocalDate { val now = now() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetFactory.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetFactory.kt index 4e0578e2..4cfc0322 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetFactory.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetFactory.kt @@ -11,7 +11,7 @@ import android.view.View.VISIBLE import android.widget.RemoteViews import android.widget.RemoteViewsService import io.github.wulkanowy.R -import io.github.wulkanowy.data.dataOrNull +import io.github.wulkanowy.data.dataOrThrow import io.github.wulkanowy.data.db.SharedPrefProvider import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student @@ -27,6 +27,7 @@ import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetProvider.Co import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetProvider.Companion.getStudentWidgetKey import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetProvider.Companion.getTodayLastLessonEndDateTimeWidgetKey import io.github.wulkanowy.utils.getCompatColor +import io.github.wulkanowy.utils.getErrorString import io.github.wulkanowy.utils.getPlural import io.github.wulkanowy.utils.toFormattedString import kotlinx.coroutines.runBlocking @@ -67,25 +68,31 @@ class TimetableWidgetFactory( override fun onDestroy() {} override fun onDataSetChanged() { - intent?.extras?.getInt(EXTRA_APPWIDGET_ID)?.let { appWidgetId -> - val date = LocalDate.ofEpochDay(sharedPref.getLong(getDateWidgetKey(appWidgetId), 0)) - val studentId = sharedPref.getLong(getStudentWidgetKey(appWidgetId), 0) + val appWidgetId = intent?.extras?.getInt(EXTRA_APPWIDGET_ID) ?: return + val date = LocalDate.ofEpochDay(sharedPref.getLong(getDateWidgetKey(appWidgetId), 0)) + val studentId = sharedPref.getLong(getStudentWidgetKey(appWidgetId), 0) + items = emptyList() + + runBlocking { runCatching { - runBlocking { - val student = getStudent(studentId) ?: return@runBlocking - val semester = semesterRepository.getCurrentSemester(student) - items = createItems( - lessons = getLessons(student, semester, date), - lastSync = timetableRepository.getLastRefreshTimestamp(semester, date, date) - ) + val student = getStudent(studentId) ?: return@runBlocking + val semester = semesterRepository.getCurrentSemester(student) + val lessons = getLessons(student, semester, date) + val lastSync = timetableRepository.getLastRefreshTimestamp(semester, date, date) + + createItems(lessons, lastSync) + } + .onFailure { + items = listOf(TimetableWidgetItem.Error(it)) + Timber.e(it, "An error has occurred in timetable widget factory") + } + .onSuccess { + items = it if (date == LocalDate.now()) { updateTodayLastLessonEnd(appWidgetId) } } - }.onFailure { - Timber.e(it, "An error has occurred in timetable widget factory") - } } } @@ -98,7 +105,7 @@ class TimetableWidgetFactory( student: Student, semester: Semester, date: LocalDate ): List { val timetable = timetableRepository.getTimetable(student, semester, date, date, false) - val lessons = timetable.toFirstResult().dataOrNull?.lessons.orEmpty() + val lessons = timetable.toFirstResult().dataOrThrow.lessons return lessons.sortedBy { it.number } } @@ -110,6 +117,7 @@ class TimetableWidgetFactory( BETWEEN_AND_BEFORE_LESSONS -> 0 else -> null } + return buildList { lessons.forEach { if (prefRepository.showTimetableGaps != NO_GAPS && prevNum != null && it.number > prevNum!! + 1) { @@ -133,15 +141,12 @@ class TimetableWidgetFactory( sharedPref.putLong(key, todayLastLessonEnd.epochSecond, true) } - companion object { - const val TIME_FORMAT_STYLE = "HH:mm" - } - override fun getViewAt(position: Int): RemoteViews? { return when (val item = items.getOrNull(position) ?: return null) { is TimetableWidgetItem.Normal -> getNormalItemRemoteView(item) is TimetableWidgetItem.Empty -> getEmptyItemRemoteView(item) is TimetableWidgetItem.Synchronized -> getSynchronizedItemRemoteView(item) + is TimetableWidgetItem.Error -> getErrorItemRemoteView(item) } } @@ -213,6 +218,18 @@ class TimetableWidgetFactory( } } + private fun getErrorItemRemoteView(item: TimetableWidgetItem.Error): RemoteViews { + return RemoteViews( + context.packageName, + R.layout.item_widget_timetable_error + ).apply { + setTextViewText( + R.id.timetable_widget_item_error_message, + context.resources.getErrorString(item.error) + ) + } + } + private fun updateTheme() { when (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) { Configuration.UI_MODE_NIGHT_YES -> { @@ -300,4 +317,8 @@ class TimetableWidgetFactory( synchronizationTime, ) } + + private companion object { + private const val TIME_FORMAT_STYLE = "HH:mm" + } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetItem.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetItem.kt index 166b1a8f..fb02f891 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetItem.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetItem.kt @@ -17,10 +17,15 @@ sealed class TimetableWidgetItem(val type: TimetableWidgetItemType) { data class Synchronized( val timestamp: Instant, ) : TimetableWidgetItem(TimetableWidgetItemType.SYNCHRONIZED) + + data class Error( + val error: Throwable + ) : TimetableWidgetItem(TimetableWidgetItemType.ERROR) } enum class TimetableWidgetItemType { NORMAL, EMPTY, SYNCHRONIZED, + ERROR, } 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 a4c2537a..1c229051 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/ExceptionExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/ExceptionExtension.kt @@ -3,6 +3,8 @@ 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 @@ -32,8 +34,10 @@ 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 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 is SSLHandshakeException -> when { error.isCausedByCertificateNotValidNow() -> R.string.error_invalid_device_datetime else -> R.string.error_timeout diff --git a/app/src/main/java/io/github/wulkanowy/utils/SdkExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/SdkExtension.kt index 889d64ea..9b6ca706 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/SdkExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/SdkExtension.kt @@ -1,5 +1,6 @@ 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 @@ -11,6 +12,7 @@ fun Sdk.init(student: Student): Sdk { schoolSymbol = student.schoolSymbol studentId = student.studentId classId = student.classId + emptyCookieJarInterceptor = true if (Sdk.Mode.valueOf(student.loginMode) == Sdk.Mode.HEBE) { mobileBaseUrl = student.mobileBaseUrl @@ -29,3 +31,12 @@ fun Sdk.init(student: Student): Sdk { 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 new file mode 100644 index 00000000..3d41c711 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/utils/WebkitCookieManagerProxy.kt @@ -0,0 +1,70 @@ +package io.github.wulkanowy.utils + +import android.util.AndroidRuntimeException +import java.net.CookiePolicy +import java.net.CookieStore +import java.net.HttpCookie +import java.net.URI +import android.webkit.CookieManager as WebkitCookieManager +import java.net.CookieManager as JavaCookieManager + +class WebkitCookieManagerProxy : JavaCookieManager(null, CookiePolicy.ACCEPT_ALL) { + + private val webkitCookieManager: WebkitCookieManager? = getWebkitCookieManager() + + /** + * @see [https://stackoverflow.com/a/70354583/6695449] + */ + private fun getWebkitCookieManager(): WebkitCookieManager? { + return try { + WebkitCookieManager.getInstance() + } catch (e: AndroidRuntimeException) { + null + } + } + + override fun put(uri: URI?, responseHeaders: Map>?) { + if (uri == null || responseHeaders == null) return + val url = uri.toString() + for (headerKey in responseHeaders.keys) { + if (headerKey == null || !( + headerKey.equals("Set-Cookie2", ignoreCase = true) || + headerKey.equals("Set-Cookie", ignoreCase = true) + ) + ) continue + + // process each of the headers + for (headerValue in responseHeaders[headerKey].orEmpty()) { + webkitCookieManager?.setCookie(url, headerValue) + } + } + } + + override operator fun get( + uri: URI?, + requestHeaders: Map?>? + ): Map> { + require(!(uri == null || requestHeaders == null)) { "Argument is null" } + val res = mutableMapOf>() + val cookie = webkitCookieManager?.getCookie(uri.toString()) + if (cookie != null) res["Cookie"] = listOf(cookie) + return res + } + + override fun getCookieStore(): CookieStore { + val cookies = super.getCookieStore() + return object : CookieStore { + override fun add(uri: URI?, cookie: HttpCookie?) = cookies.add(uri, cookie) + override fun get(uri: URI?): List = cookies.get(uri) + override fun getCookies(): List = cookies.cookies + override fun getURIs(): List = cookies.urIs + override fun remove(uri: URI?, cookie: HttpCookie?): Boolean = + cookies.remove(uri, cookie) + + override fun removeAll(): Boolean { + webkitCookieManager?.removeAllCookies(null) ?: return false + return true + } + } + } +} diff --git a/app/src/main/java/io/github/wulkanowy/utils/security/Scrambler.kt b/app/src/main/java/io/github/wulkanowy/utils/security/Scrambler.kt index c994ebab..db16a256 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/security/Scrambler.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/security/Scrambler.kt @@ -16,6 +16,7 @@ import android.util.Base64.DEFAULT import android.util.Base64.decode import android.util.Base64.encode import android.util.Base64.encodeToString +import dagger.hilt.android.qualifiers.ApplicationContext import timber.log.Timber import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream @@ -33,108 +34,124 @@ import javax.crypto.CipherInputStream import javax.crypto.CipherOutputStream import javax.crypto.spec.OAEPParameterSpec import javax.crypto.spec.PSource.PSpecified +import javax.inject.Inject +import javax.inject.Singleton import javax.security.auth.x500.X500Principal -private const val KEYSTORE_NAME = "AndroidKeyStore" +@Singleton +class Scrambler @Inject constructor( + @ApplicationContext private val context: Context, +) { + private val keyCharset = Charset.forName("UTF-8") -private const val KEY_ALIAS = "wulkanowy_password" + private val isKeyPairExists: Boolean + get() = keyStore.getKey(KEY_ALIAS, null) != null -private val KEY_CHARSET = Charset.forName("UTF-8") + private val keyStore: KeyStore + get() = KeyStore.getInstance(KEYSTORE_NAME).apply { load(null) } -private val isKeyPairExists: Boolean - get() = keyStore.getKey(KEY_ALIAS, null) != null + private val cipher: Cipher + get() { + return if (SDK_INT >= M) Cipher.getInstance( + "RSA/ECB/OAEPWithSHA-256AndMGF1Padding", + "AndroidKeyStoreBCWorkaround" + ) + else Cipher.getInstance("RSA/ECB/PKCS1Padding", "AndroidOpenSSL") + } -private val keyStore: KeyStore - get() = KeyStore.getInstance(KEYSTORE_NAME).apply { load(null) } + fun encrypt(plainText: String): String { + if (plainText.isEmpty()) throw ScramblerException("Text to be encrypted is empty") -private val cipher: Cipher - get() { - return if (SDK_INT >= M) Cipher.getInstance( - "RSA/ECB/OAEPWithSHA-256AndMGF1Padding", - "AndroidKeyStoreBCWorkaround" - ) - else Cipher.getInstance("RSA/ECB/PKCS1Padding", "AndroidOpenSSL") + return try { + if (!isKeyPairExists) generateKeyPair() + + cipher.let { + if (SDK_INT >= M) { + OAEPParameterSpec("SHA-256", "MGF1", SHA1, PSpecified.DEFAULT).let { spec -> + it.init(ENCRYPT_MODE, keyStore.getCertificate(KEY_ALIAS).publicKey, spec) + } + } else it.init(ENCRYPT_MODE, keyStore.getCertificate(KEY_ALIAS).publicKey) + + ByteArrayOutputStream().let { output -> + CipherOutputStream(output, it).apply { + write(plainText.toByteArray(keyCharset)) + close() + } + encodeToString(output.toByteArray(), DEFAULT) + } + } + } catch (exception: Exception) { + Timber.e(exception, "An error occurred while encrypting text") + String(encode(plainText.toByteArray(keyCharset), DEFAULT), keyCharset) + } } -fun encrypt(plainText: String, context: Context): String { - if (plainText.isEmpty()) throw ScramblerException("Text to be encrypted is empty") + fun decrypt(cipherText: String): String { + if (cipherText.isEmpty()) throw ScramblerException("Text to be encrypted is empty") - return try { - if (!isKeyPairExists) generateKeyPair(context) + return try { + if (!isKeyPairExists) throw ScramblerException("KeyPair doesn't exist") - cipher.let { - if (SDK_INT >= M) { - OAEPParameterSpec("SHA-256", "MGF1", SHA1, PSpecified.DEFAULT).let { spec -> - it.init(ENCRYPT_MODE, keyStore.getCertificate(KEY_ALIAS).publicKey, spec) + cipher.let { + if (SDK_INT >= M) { + OAEPParameterSpec("SHA-256", "MGF1", SHA1, PSpecified.DEFAULT).let { spec -> + it.init(DECRYPT_MODE, keyStore.getKey(KEY_ALIAS, null), spec) + } + } else it.init(DECRYPT_MODE, keyStore.getKey(KEY_ALIAS, null)) + + CipherInputStream( + ByteArrayInputStream(decode(cipherText, DEFAULT)), + it + ).let { input -> + val values = ArrayList() + var nextByte: Int + while (run { nextByte = input.read(); nextByte } != -1) { + values.add(nextByte.toByte()) + } + val bytes = ByteArray(values.size) + for (i in bytes.indices) { + bytes[i] = values[i] + } + String(bytes, 0, bytes.size, keyCharset) } - } else it.init(ENCRYPT_MODE, keyStore.getCertificate(KEY_ALIAS).publicKey) + } + } catch (e: Exception) { + throw ScramblerException("An error occurred while decrypting text", e) + } + } - ByteArrayOutputStream().let { output -> - CipherOutputStream(output, it).apply { - write(plainText.toByteArray(KEY_CHARSET)) - close() - } - encodeToString(output.toByteArray(), DEFAULT) + private fun generateKeyPair() { + (if (SDK_INT >= M) { + KeyGenParameterSpec.Builder(KEY_ALIAS, PURPOSE_DECRYPT or PURPOSE_ENCRYPT) + .setDigests(DIGEST_SHA256, DIGEST_SHA512) + .setEncryptionPaddings(ENCRYPTION_PADDING_RSA_OAEP) + .setCertificateSerialNumber(BigInteger.TEN) + .setCertificateSubject(X500Principal("CN=Wulkanowy")) + .build() + } else { + KeyPairGeneratorSpec.Builder(context) + .setAlias(KEY_ALIAS) + .setSubject(X500Principal("CN=Wulkanowy")) + .setSerialNumber(BigInteger.TEN) + .setStartDate(Calendar.getInstance().time) + .setEndDate(Calendar.getInstance().apply { add(YEAR, 99) }.time) + .build() + }).let { + KeyPairGenerator.getInstance("RSA", KEYSTORE_NAME).apply { + initialize(it) + genKeyPair() } } - } catch (exception: Exception) { - Timber.e(exception, "An error occurred while encrypting text") - String(encode(plainText.toByteArray(KEY_CHARSET), DEFAULT), KEY_CHARSET) + Timber.i("A new KeyPair has been generated") + } + + fun clearKeyPair() { + keyStore.deleteEntry(KEY_ALIAS) + Timber.i("KeyPair has been cleared") + } + + private companion object { + private const val KEYSTORE_NAME = "AndroidKeyStore" + private const val KEY_ALIAS = "wulkanowy_password" } } - -fun decrypt(cipherText: String): String { - if (cipherText.isEmpty()) throw ScramblerException("Text to be encrypted is empty") - - return try { - if (!isKeyPairExists) throw ScramblerException("KeyPair doesn't exist") - - cipher.let { - if (SDK_INT >= M) { - OAEPParameterSpec("SHA-256", "MGF1", SHA1, PSpecified.DEFAULT).let { spec -> - it.init(DECRYPT_MODE, keyStore.getKey(KEY_ALIAS, null), spec) - } - } else it.init(DECRYPT_MODE, keyStore.getKey(KEY_ALIAS, null)) - - CipherInputStream(ByteArrayInputStream(decode(cipherText, DEFAULT)), it).let { input -> - val values = ArrayList() - var nextByte: Int - while (run { nextByte = input.read(); nextByte } != -1) { - values.add(nextByte.toByte()) - } - val bytes = ByteArray(values.size) - for (i in bytes.indices) { - bytes[i] = values[i] - } - String(bytes, 0, bytes.size, KEY_CHARSET) - } - } - } catch (e: Exception) { - throw ScramblerException("An error occurred while decrypting text", e) - } -} - -private fun generateKeyPair(context: Context) { - (if (SDK_INT >= M) { - KeyGenParameterSpec.Builder(KEY_ALIAS, PURPOSE_DECRYPT or PURPOSE_ENCRYPT) - .setDigests(DIGEST_SHA256, DIGEST_SHA512) - .setEncryptionPaddings(ENCRYPTION_PADDING_RSA_OAEP) - .setCertificateSerialNumber(BigInteger.TEN) - .setCertificateSubject(X500Principal("CN=Wulkanowy")) - .build() - } else { - KeyPairGeneratorSpec.Builder(context) - .setAlias(KEY_ALIAS) - .setSubject(X500Principal("CN=Wulkanowy")) - .setSerialNumber(BigInteger.TEN) - .setStartDate(Calendar.getInstance().time) - .setEndDate(Calendar.getInstance().apply { add(YEAR, 99) }.time) - .build() - }).let { - KeyPairGenerator.getInstance("RSA", KEYSTORE_NAME).apply { - initialize(it) - genKeyPair() - } - } - Timber.i("A new KeyPair has been generated") -} 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 89f97bdc..ef6308b6 100644 --- a/app/src/main/play/release-notes/pl-PL/default.txt +++ b/app/src/main/play/release-notes/pl-PL/default.txt @@ -1,6 +1,7 @@ -Wersja 2.3.0 +Wersja 2.4.2 -— poprawiliśmy kilka usterek przy odświeżaniu danych (ale pewnie nie wszystkie) -— zaktualizowaliśmy sposób pytania o zgodę na personalizowane reklamy +- naprawiliśmy crash przy przełączaniu uczniów, motywów i języków +- naprawiliśmy crash przy dodawaniu dodatkowych lekcji +- naprawiliśmy obsługę błędów widżetach Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases diff --git a/app/src/main/res/drawable/ic_close.xml b/app/src/main/res/drawable/ic_close.xml new file mode 100644 index 00000000..1d6c0046 --- /dev/null +++ b/app/src/main/res/drawable/ic_close.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index d14de50a..a9284234 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 d832490a..b9c63539 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 new file mode 100644 index 00000000..539aa0cc --- /dev/null +++ b/app/src/main/res/layout/dialog_captcha.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_conference.xml b/app/src/main/res/layout/dialog_conference.xml index 72837b81..3d33fd56 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 519d1531..e2f77e1f 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 f47f6108..8606a5ce 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 8c6cf0a7..10b71907 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 e0ff5b74..dc7ae32d 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 3a1d3fd0..fc32a252 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 9b81737f..c526ed74 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 9c8b18b3..3b88ea5f 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 4e0ef556..a771b772 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 aeb01b3b..de269648 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_dashboard.xml b/app/src/main/res/layout/fragment_dashboard.xml index 348602d7..9de44a70 100644 --- a/app/src/main/res/layout/fragment_dashboard.xml +++ b/app/src/main/res/layout/fragment_dashboard.xml @@ -18,7 +18,8 @@ + android:layout_height="match_parent" + tools:visibility="gone"> - + + @@ -55,14 +70,21 @@ android:gravity="center" android:padding="8dp" android:text="@string/error_unknown" - android:textSize="20sp" /> + android:textSize="20sp" + app:layout_constraintBottom_toTopOf="@id/dashboard_error_buttons" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/dashboard_error_image" /> + android:orientation="horizontal" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintTop_toBottomOf="@id/dashboard_error_message"> - + diff --git a/app/src/main/res/layout/fragment_login_form.xml b/app/src/main/res/layout/fragment_login_form.xml index fc5e5f35..10864e64 100644 --- a/app/src/main/res/layout/fragment_login_form.xml +++ b/app/src/main/res/layout/fragment_login_form.xml @@ -261,6 +261,7 @@ android:layout_marginEnd="24dp" android:layout_marginRight="24dp" android:hint="@string/login_domain_suffix_hint" + app:errorEnabled="true" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/loginFormHostLayout" diff --git a/app/src/main/res/layout/header_grade_details.xml b/app/src/main/res/layout/header_grade_details.xml index f2ba9a8c..e43e8993 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_dashboard_admin_message.xml b/app/src/main/res/layout/item_dashboard_admin_message.xml index e12241df..407e1292 100644 --- a/app/src/main/res/layout/item_dashboard_admin_message.xml +++ b/app/src/main/res/layout/item_dashboard_admin_message.xml @@ -34,11 +34,24 @@ android:layout_marginEnd="16dp" android:textSize="18sp" android:textStyle="bold" - app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintEnd_toStartOf="@id/dashboard_admin_message_item_close" app:layout_constraintStart_toEndOf="@id/dashboard_admin_message_item_icon" app:layout_constraintTop_toTopOf="parent" tools:text="@tools:sample/lorem" /> + + + android:layout_height="1dp" + android:id="@+id/gradeSummaryItemPointsDivider" + android:background="@drawable/ic_all_divider" /> + android:id="@+id/gradeSummaryItemPredictedDivider" + android:layout_height="1dp" + android:background="@drawable/ic_all_divider" /> + android:layout_height="1dp" + android:id="@+id/gradeSummaryItemFinalDivider" + android:background="@drawable/ic_all_divider" /> + + + + + + + diff --git a/app/src/main/res/layout/item_timetable.xml b/app/src/main/res/layout/item_timetable.xml index 57af6f7e..b9966c12 100644 --- a/app/src/main/res/layout/item_timetable.xml +++ b/app/src/main/res/layout/item_timetable.xml @@ -1,7 +1,6 @@ @@ -49,8 +49,9 @@ android:maxLines="1" android:textColor="?android:textColorSecondary" android:textSize="13sp" + app:layout_constraintBottom_toTopOf="@id/timetableItemTimeFinish" app:layout_constraintStart_toEndOf="@id/timetableItemNumber" - app:layout_constraintTop_toTopOf="@id/timetableItemNumber" + app:layout_constraintTop_toTopOf="parent" tools:text="11:11" /> @@ -83,13 +91,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 +107,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" /> diff --git a/app/src/main/res/layout/item_widget_timetable_error.xml b/app/src/main/res/layout/item_widget_timetable_error.xml new file mode 100644 index 00000000..6f9ab067 --- /dev/null +++ b/app/src/main/res/layout/item_widget_timetable_error.xml @@ -0,0 +1,12 @@ + + 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 6800b72e..3684c267 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/layout/widget_timetable.xml b/app/src/main/res/layout/widget_timetable.xml index b07cc78f..b438da6c 100644 --- a/app/src/main/res/layout/widget_timetable.xml +++ b/app/src/main/res/layout/widget_timetable.xml @@ -114,7 +114,5 @@ android:text="@string/widget_timetable_no_items" android:textAppearance="?attr/textAppearanceBody1" android:visibility="gone" /> - - diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 3b8d144e..218bf58e 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -13,6 +13,7 @@ Prohlížeč protokolů Ladění Ladění oznámení + Vymazat soubory cookie webview Tvůrci Licence Zprávy @@ -55,6 +56,7 @@ Neplatný e-mail Místo e-mailu použijte přiřazené přihlašovací údaje Použijte přiřazené přihlašovací nebo e-mail v @%1$s + Invalid domain suffix Neplatný symbol. Pokud jej nemůžete najít, kontaktujte školu Nevymýšlejte si! Pokud symbol nemůžete najít, kontaktujte školu Žák nebyl nalezen. Zkontrolujte správnost symbolu a vybrané varianty deníku UONET+ @@ -96,6 +98,8 @@ Přihlásit se Relace vypršela Relace vypršela. Přihlaste se prosím znovu + Heslo k vašemu účtu bylo změněno. Musíte se znovu přihlásit do Wulkanového + Heslo bylo změněno Podpora aplikace Líbí se Vám tato aplikace? Podpořte její vývoj tím, že povolíte neinvazivní reklamy, které můžete kdykoliv vypnout Zapnout reklamy @@ -114,6 +118,7 @@ Součet bodů Konečná známka Předpokládaná známka + Popisná známka Vypočítaný průměr Jak funguje vypočítaný průměr? Vypočítaný průměr je aritmetický průměr vypočítaný z průměrů předmětů. Umožňuje vám to znát přibližný konečný průměr. Vypočítává se způsobem zvoleným uživatelem v nastavení aplikaci. Doporučuje se vybrat příslušnou možnost. Důvodem je rozdílný výpočet školních průměrů. Pokud vaše škola navíc uvádí průměr předmětů na stránce deníku Vulcan, aplikace si je stáhne a tyto průměry nepočítá. To lze změnit vynucením výpočtu průměru v nastavení aplikaci.\n\nPrůměr známek pouze z vybraného semestru:\n1. Výpočet váženého průměru pro každý předmět v daném semestru\n2. Sčítání vypočítaných průměrů\n3. Výpočet aritmetického průměru součtených průměrů\n\nPrůměr průměrů z obou semestrů:\n1. Výpočet váženého průměru pro každý předmět v semestru 1 a 2\n2. Výpočet aritmetického průměru vypočítaných průměrů za semestry 1 a 2 pro každý předmět.\n3. Sčítání vypočítaných průměrů\n4. Výpočet aritmetického průměru sečtených průměrů\n\nPrůměr známek z celého roku:\n1. Výpočet váženého průměru za rok pro každý předmět. Konečný průměr v 1. semestru je nepodstatný.\n2. Sčítání vypočítaných průměrů\n3. Výpočet aritmetického průměru součtených průměrů @@ -157,6 +162,12 @@ Nové konečné známky Nové konečné známky + + Nová popisná známka + Nové popisné známky + Nové popisné známky + Nové popisné známky + Máte %1$d novou známku Máte %1$d nové známky @@ -175,6 +186,12 @@ Máte %1$d nových konečných známek Máte %1$d nových konečných známek + + Máte %1$d novou popisnou známku + Máte %1$d nové popisné známky + Máte %1$d nových popisných známek + Máte %1$d nových popisných známek + Lekce Učebna @@ -760,7 +777,7 @@ Podpora Ochrana osobních údajů Souhlasy - Show consent to data processing + Zobrazit souhlas se zpracováním údajů Zobrazit reklamy v aplikaci Podívejte se na jednu reklamu pro podporu projektu Souhlas se zpracováním dat @@ -832,6 +849,9 @@ Autorizace Pro provoz aplikace potřebujeme potvrdit vaši identitu. Zadejte PESEL žáka <b>%1$s</b> v níže uvedeném poli Zatím přeskočit + + Probíhá ověřování. Počkejte… + Úspěšně ověřeno Žádné internetové připojení Vyskytla se chyba. Zkontrolujte hodiny svého zařízení @@ -841,6 +861,7 @@ Probíhá údržba deníku UONET+. Zkuste to později znovu Neznámá chyba deniku UONET+. Prosím zkuste to znovu později Neznámá chyba aplikace. Prosím zkuste to znovu později + Vyžadováno ověření Captcha Vyskytla se neočekávaná chyba Funkce je deaktivována přes vaší školou Funkce není k dispozici. Přihlaste se v jiném režimu než Mobile API diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 2e3a9dec..13828481 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -10,9 +10,10 @@ Einstellungen Mehr Über die Applikation - Log Viewer + Log viewer Debuggen Benachrichtigungen debuggen + Clear webview cookies Mitarbeiter Lizenzen Nachrichten @@ -55,6 +56,7 @@ Ungültige email Den zugewiesenen Login anstelle von email verwenden Benutze den zugewiesenen Login oder E-Mail in @%1$s + Invalid domain suffix Invalid symbol. If you cannot find it, please contact the school Don\'t make this up! If you cannot find it, please contact the school Schüler nicht gefunden. Überprüfen Sie das Symbol und die gewählte Variation des UONET+ Registers @@ -96,6 +98,8 @@ Anmelden Die Sitzung ist abgelaufen Die Sitzung ist abgelaufen, bitte loggen Sie sich erneut ein + Your account password has been changed. You need to log in to Wulkanowy again + Password changed Anwendungsunterstützung Gefällt Ihnen diese App? Unterstützen Sie ihre Entwicklung, indem Sie nicht-invasive Werbung aktivieren, die Sie jederzeit deaktivieren können Werbung aktivieren @@ -114,6 +118,7 @@ Gesamtpunkte Finaler Note Vorhergesagte Note + Descriptive grade Berechnender Durchschnitt Wie funktioniert der berechnete Durchschnitt? Der berechnete Mittelwert ist das arithmetische Mittel, das aus den Durchschnittswerten der Probanden errechnet wird. Es erlaubt Ihnen, den ungefähre endgültigen Durchschnitt zu kennen. Sie wird auf eine vom Anwender in den Anwendungseinstellungen gewählte Weise berechnet. Es wird empfohlen, die entsprechende Option zu wählen. Das liegt daran, dass die Berechnung der Schuldurchschnitte unterschiedlich ist. Wenn Ihre Schule den Durchschnitt der Fächer auf der Vulcan-Seite angibt, lädt die Anwendung diese Fächer herunter und berechnet nicht den Durchschnitt. Dies kann geändert werden, indem die Berechnung des Durchschnitts in den Anwendungseinstellungen erzwungen wird. \n\nDurchschnitt der Noten nur aus dem ausgewählten Semester :\n1. Berechnung des gewichteten Durchschnitts für jedes Fach in einem bestimmten Semester\n2. Addition der berechneten Durchschnittswerte\n3. Berechnung des arithmetischen Mittels der summierten Durchschnitte\nDurchschnitt der Durchschnitte aus beiden Semestern:\n1. Berechnung des gewichteten Durchschnitts für jedes Fach in Semester 1 und 2\n2. Berechnung des arithmetischen Mittels der berechneten Durchschnitte für Semester 1 und 2 für jedes Fach. \n3. Hinzufügen von berechneten Durchschnittswerten\n4. Berechnung des arithmetischen Mittels der summierten Durchschnitte\nDurchschnitt der Noten aus dem ganzen Jahr:\n1. Berechnung des gewichteten Jahresdurchschnitts für jedes Fach. Der Abschlussdurchschnitt im 1. Semester ist irrelevant. \n2. Addition der berechneten Durchschnittswerte\n3. Berechnung des arithmetischen Mittels der summierten Mittelwerte @@ -149,6 +154,10 @@ Neue Abschlussnote Neue Abschlussnoten + + New descriptive grade + New descriptive grades + Du hast %1$d Note bekommen Du hast %1$d Noten bekommen @@ -161,6 +170,10 @@ Sie haben %1$d Abschlussnote bekommen Sie haben %1$d Abschlussnoten bekommen + + You received %1$d descriptive grade + You received %1$d descriptive grades + Lektion Klassenzimmer @@ -742,6 +755,9 @@ Authorization To operate the application, we need to confirm your identity. Please enter the student\'s PESEL <b>%1$s</b> in the field below Skip for now + + Verification is in progress. Wait… + Verified successfully Keine Internetverbindung Es ist ein Fehler aufgetreten. Überprüfen Sie Ihre Geräteuhr @@ -751,6 +767,7 @@ Wartung im Gange UONET + Klassenbuch. Versuchen Sie es später noch einmal Unbekannter UONET + Registerfehler. Versuchen Sie es später erneut Unbekannter Anwendungsfehler. Bitte versuchen Sie es später noch einmal + Captcha verification required Ein unerwarteter Fehler ist aufgetreten Funktion, die von Ihrer Schule deaktiviert wurde Feature in diesem Modus nicht verfügbar diff --git a/app/src/main/res/values-night-v31/styles.xml b/app/src/main/res/values-night-v31/styles.xml index 6e6c4d79..808bc714 100644 --- a/app/src/main/res/values-night-v31/styles.xml +++ b/app/src/main/res/values-night-v31/styles.xml @@ -32,6 +32,9 @@ @color/material_dynamic_primary40 @color/timetable_canceled_dark @color/timetable_change_dark + @color/attendance_absence_dark + @color/attendance_lateness_dark + @color/colorErrorLight @color/colorDividerInverse @color/material_dynamic_secondary20 diff --git a/app/src/main/res/values-night/styles.xml b/app/src/main/res/values-night/styles.xml index 5d9aa22a..5840a051 100644 --- a/app/src/main/res/values-night/styles.xml +++ b/app/src/main/res/values-night/styles.xml @@ -21,6 +21,8 @@ @color/colorSurfaceDark @color/timetable_canceled_dark @color/timetable_change_dark + @color/attendance_absence_dark + @color/attendance_lateness_dark @color/colorErrorLight @color/colorDividerInverse @color/colorSwipeRefreshDark diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 6842538e..fbf37af5 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -13,6 +13,7 @@ Przeglądarka logów Debugowanie Debugowanie powiadomień + Wyczyść ciasteczka webview Twórcy Licencje Wiadomości @@ -55,6 +56,7 @@ Nieprawidłowy adres e-mail Użyj loginu zamiast adresu e-mail Użyj loginu lub adresu e-mail w @%1$s + Nieprawidłowy sufiks domeny Nieprawidłowy symbol. Jeśli nie możesz go znaleźć, skontaktuj się ze szkołą Nie zmyślaj! Jeśli nie możesz znaleźć symbolu, skontaktuj się ze szkołą Nie znaleziono ucznia. Sprawdź poprawność symbolu i wybranej odmiany dziennika UONET+ @@ -96,6 +98,8 @@ Zaloguj się Sesja wygasła Sesja wygasła, zaloguj się ponownie + Hasło do Twojego konta zostało zmienione. Musisz zalogować się ponownie do Wulkanowego + Hasło zostało zmienione Wparcie aplikacji Podoba Ci się ta aplikacja? Wspieraj jej rozwój poprzez włączenie nieinwazyjnych reklam, które możesz wyłączyć w dowolnym momencie Włącz reklamy @@ -114,6 +118,7 @@ Suma punktów Ocena końcowa Przewidywana ocena + Ocena opisowa Obliczona średnia Jak działa obliczona średnia? Obliczona średnia jest średnią arytmetyczną obliczoną ze średnich przedmiotów. Pozwala ona na poznanie przybliżonej średniej końcowej. Jest obliczana w sposób wybrany przez użytkownika w ustawieniach aplikacji. Zaleca się wybranie odpowiedniej opcji. Dzieje się tak dlatego, że obliczanie średnich w szkołach różni się. Dodatkowo, jeśli twoja szkoła ma włączone średnie przedmiotów na stronie dziennika Vulcan, aplikacja pobiera je i ich nie oblicza. Można to zmienić, wymuszając obliczanie średniej w ustawieniach aplikacji.\n\nŚrednia ocen tylko z wybranego semestru:\n1. Obliczanie średniej arytmetycznej każdego przedmiotu w danym semestrze\n2. Zsumowanie obliczonych średnich\n3. Obliczanie średniej arytmetycznej zsumowanych średnich\n\nŚrednia ze średnich z obu semestrów:\n1.Obliczanie średniej arytmetycznej każdego przedmiotu w semestrze 1 i 2\n2. Obliczanie średniej arytmetycznej obliczonych średnich w semestrze 1 i 2 każdego przedmiotu.\n3. Zsumowanie obliczonych średnich\n4. Obliczanie średniej arytmetycznej zsumowanych średnich\n\nŚrednia wszystkich ocen z całego roku:\n1. Obliczanie średniej arytmetycznej z każdego przedmiotu w ciągu całego roku. Końcowa ocena w 1 semestrze jest bez znaczenia.\n2. Zsumowanie obliczonych średnich\n3. Obliczanie średniej arytmetycznej z zsumowanych średnich @@ -157,6 +162,12 @@ Nowe oceny końcowe Nowe oceny końcowe + + Nowa ocena opisowa + Nowe oceny opisowe + Nowe oceny opisowe + Nowe oceny opisowe + Masz %1$d nową ocenę Masz %1$d nowe oceny @@ -175,6 +186,12 @@ Masz %1$d nowych końcowych ocen Masz %1$d nowych końcowych ocen + + Masz %1$d nową ocenę opisową + Masz %1$d nowe oceny opisowe + Masz %1$d nowych ocen opisowych + Masz %1$d nowych ocen opisowych + Lekcja Sala @@ -832,6 +849,9 @@ Autoryzacja Rodzicu, musimy mieć pewność, że Twój adres e-mail został powiązany z prawidłowym kontem ucznia. W celu autoryzacji konta podaj numer PESEL ucznia <b>%1$s</b> w polu poniżej Na razie pomiń + + Trwa weryfikacja. Czekaj… + Pomyślnie zweryfikowano Brak połączenia z internetem Wystąpił błąd. Sprawdź poprawność daty w urządzeniu @@ -841,6 +861,7 @@ Trwa przerwa techniczna dziennika UONET+. Spróbuj ponownie później Nieznany błąd dziennika UONET+. Spróbuj ponownie później Nieznany błąd aplikacji. Spróbuj ponownie później + Wymagana weryfikacja captcha Wystąpił nieoczekiwany błąd Funkcja wyłączona przez szkołę Funkcja niedostępna. Zaloguj się w trybie innym niż Mobilne API diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index ec16e3e3..b21db8c5 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -13,6 +13,7 @@ Просмотр журнала Отладка Отладка уведомлений + Clear webview cookies Разработчики Лицензии Сообщения @@ -55,6 +56,7 @@ Неверный e-mail Используйте назначенный логин вместо e-mail Используйте назначенный логин или email в @%1$s + Invalid domain suffix Invalid symbol. If you cannot find it, please contact the school Don\'t make this up! If you cannot find it, please contact the school Ученик не найден. Проверьте symbol и выбранный тип дненика UONET+ @@ -96,6 +98,8 @@ Войти Сеанс истёк Сеанс истёк, авторизуйтесь снова + Your account password has been changed. You need to log in to Wulkanowy again + Password changed Поддержка приложения Вам нравится это приложение? Поддержите его разработку, включив неинвазивную рекламу, которую можно отключить в любое время Включить рекламу @@ -114,6 +118,7 @@ Сумма баллов Итоговая оценка Ожидаемая оценка + Descriptive grade Рассчитанная средняя оценка Как работает \"Рассчитанная средняя оценка\"? Рассчитанная средняя оценка - это среднее арифметическое, рассчитанное на основе средних оценок по предметам. Это позволяет узнать приблизительную итоговую среднюю оценку. Она рассчитывается способом, выбранным пользователем в настройках приложения. Рекомендуется выбрать подходящий вариант, так как каждая школа по разному считает среднюю оценку. Кроме того, если ваша школа выставляет средние оценки по предметам на странице Vulcan, приложение просто загрузит их. Это можно изменить, заставив приложение считать среднюю оценку в настройках.\n\nСредняя из оценок выбранного семестра:\n1. Вычисление средневзвешенного значения по каждому предмету за семестр\n2.Суммирование вычисленных значений\n3. Вычисление среднего арифметического суммированных значений\n\nСредняя из средних оценок семестров:\n1.Расчет средневзвешенного значения для каждого предмета в семестрах. \n2. Вычисление среднего арифметического из средневзвешенных значений для каждого предмета в семестрах.\n3. Суммирование средних арифметических\n4. Вычисление среднего арифматического из суммированных значений\n\nСредняя из оценок со всего года:\n1. Расчет средневзвешенного значения по каждому предмету за год. Итоговое среднее значение за 1 семестр не имеет значения.\n2. Суммирование вычисленных средних\n3. Расчет среднего арифметического суммированных чисел @@ -157,6 +162,12 @@ Новые итоговые оценки Новые итоговые оценки + + New descriptive grade + New descriptive grades + New descriptive grades + New descriptive grades + Вы получили %1$d новую оценку Вы получили %1$d новые оценки @@ -175,6 +186,12 @@ Вы получили %1$d новых итоговых оценок Вы получили %1$d новых итоговые оценки + + You received %1$d descriptive grade + You received %1$d descriptive grades + You received %1$d descriptive grades + You received %1$d descriptive grades + Урок Аудитория @@ -827,11 +844,14 @@ Авторизация отклонена. Предоставленные данные не соответствуют записям в кабинете секретаря. Неправильный номер PESEL Номер PESEL - Authorize + Авторизовать Авторизация прошла успешно Авторизация Для работы приложения нам необходимо подтвердить вашу личность. Введите PESEL учащегося <b>%1$s</b> в поле ниже Пропустить сейчас + + Verification is in progress. Wait… + Verified successfully Интернет-соединение отсутствует Произошла ошибка. Проверьте время на вашем устройстве @@ -841,6 +861,7 @@ UONET+ проводит техническое обслуживание, повторите попытку позже Неизвестная ошибка дневника UONET+, повторите попытку позже Неизвестная ошибка приложения, повторите попытку позже + Captcha verification required Произошла непредвиденная ошибка Функция отключена вашей школой Функция недоступна в режиме Mobile API. Воспользуйтесь другим режимом diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 6bca5b9c..973758cc 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -13,6 +13,7 @@ Prehliadač protokolov Ladenie Ladenie oznámení + Vymazať súbory cookie webview Tvorcovia Licencie Správy @@ -55,6 +56,7 @@ Neplatný e-mail Namiesto e-mailu použite priradené prihlasovacie údaje Použite priradené prihlasovacie alebo e-mail v @%1$s + Invalid domain suffix Neplatný symbol. Pokiaľ ho nemôžete nájsť, kontaktujte školu Nevymýšľajte si! Pokiaľ symbol nemôžete nájsť, kontaktujte školu Žiak nebol nájdený. Skontrolujte správnosť symbolu a vybrané varianty denníka UONET+ @@ -96,6 +98,8 @@ Prihlásiť sa Relácia vypršala Relácia vypršala. Prihláste sa prosím znovu + Heslo k vášmu účtu bolo zmenené. Musíte sa znovu prihlásiť do Wulkanového + Heslo bolo zmenené Podpora aplikácie Páči sa Vám táto aplikácia? Podporte jej vývoj tým, že povolíte neinvazívne reklamy, ktoré môžete kedykoľvek vypnúť Zapnúť reklamy @@ -114,6 +118,7 @@ Súčet bodov Konečná známka Predpokladaná známka + Popisná známka Vypočítaný priemer Ako funguje vypočítaný priemer? Vypočítaný priemer je aritmetický priemer vypočítaný z priemerov predmetov. Umožňuje vám to poznať približný konečný priemer. Vypočítava sa spôsobom zvoleným užívateľom v nastaveniach aplikácii. Odporúča sa vybrať príslušnú možnosť. Dôvodom je rozdielny výpočet školských priemerov. Ak vaša škola navyše uvádza priemer predmetov na stránke denníka Vulcan, aplikácia si ich stiahne a tieto priemery nepočíta. To možno zmeniť vynútením výpočtu priemeru v nastavení aplikácii.\n\nPriemer známok iba z vybraného semestra:\n1. Výpočet váženého priemeru pre každý predmet v danom semestri\n2. Sčítanie vypočítaných priemerov\n3. Výpočet aritmetického priemeru součtených priemerov\n\nPriemer priemerov z oboch semestrov:\n1. Výpočet váženého priemeru pre každý predmet v semestri 1 a 2\n2. Výpočet aritmetického priemeru vypočítaných priemerov za semestre 1 a 2 pre každý predmet.\n3. Sčítanie vypočítaných priemerov\n4. Výpočet aritmetického priemeru součtených priemerov\n\nPriemer známok z celého roka:\n1. Výpočet váženého priemeru za rok pre každý predmet. Konečný priemer v 1. semestri je nepodstatný.\n2. Sčítanie vypočítaných priemerov\n3. Výpočet aritmetického priemeru součtených priemerov @@ -157,6 +162,12 @@ Nové konečné známky Nové konečné známky + + Nová popisná známka + Nové popisné známky + Nové popisné známky + Nové popisné známky + Máte %1$d novú známku Máte %1$d nové známky @@ -175,6 +186,12 @@ Máte %1$d nových konečných známok Máte %1$d nových konečných známok + + Máte %1$d novú popisnú známku + Máte %1$d nové popisné známky + Máte %1$d nových popisných známok + Máte %1$d nových popisných známok + Lekcia Učebňa @@ -760,7 +777,7 @@ Podpora Ochrana osobných údajov Súhlasy - Show consent to data processing + Zobraziť súhlas so spracovaním údajov Zobraziť reklamy v aplikácii Pozrite sa na jednu reklamu pre podporu projektu Súhlas so spracovaním dát @@ -832,6 +849,9 @@ Autorizácia Na prevádzku aplikácie potrebujeme potvrdiť vašu identitu. Zadajte PESEL žiaka <b>%1$s</b> v nižšie uvedenom poli Zatiaľ preskočiť + + Overovanie prebieha. Počkajte… + Úspešne overené Žiadne internetové pripojenie Vyskytla sa chyba. Skontrolujte hodiny svojho zariadenia @@ -841,6 +861,7 @@ Prebieha údržba denníka UONET+. Skúste to neskôr znova Neznáma chyba dennika UONET+. Prosím skúste to znova neskôr Neznáma chyba aplikácie. Prosím skúste to znova neskôr + Vyžaduje sa overenie Captcha Vyskytla sa neočakávaná chyba Funkcia je deaktivovaná cez vašou školou Funkcia nie je k dispozícii. Prihláste sa v inom režime než Mobile API diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index b6e1290b..5fcef335 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -13,6 +13,7 @@ Переглядач логів Відладка Відладка сповіщень + Очистити кукі веб - перегляду Розробники Ліцензії Листи @@ -55,6 +56,7 @@ Недійсна адреса e-mail Використовуйте призначений логін замість адреси e-mail Використовуйте призначений логін або адресу e-mail в @%1$s + Invalid domain suffix Некоректний символ. Якщо ви не можете знайти його, будь ласка, зв\'яжіться зі школою Не вигадуйте! Якщо ви не можете знайти його, будь ласка, зв\'яжіться зі школою Студента не знайдено. Перевірте symbol та обраний тип щоденника UONET+ @@ -96,6 +98,8 @@ Увійти Минув термін дії сесії Минув термін дії сесії, авторизуйтеся знову + Пароль вашого облікового запису був змінений. Ви повинні увійти в Wulkanowy знову + Пароль змінено Підтримка додатку Вам подобається цей додаток? Підтримайте його розвиток, увімкнувши неінвазивну рекламу, яку ви можете відключити в будь-який час Увімкнути рекламу @@ -114,6 +118,7 @@ Всього балів Підсумкова оцінка Передбачувана оцінка + Описова оцінка Розрахована середня оцінка Як працює \"Розрахована середня оцінка\"? Розрахована середня оцінка - це середнє арифметичне, обчислене з середніх оцінок з предметів. Це дозволяє дізнатися приблизну кінцеву середню оцінку. Вона розраховується спосібом, обраним користувачем у налаштуваннях програми. Рекомендується вибрати відповідний варіант, тому що кожна школа по різному розраховує середню оцінку. Крім того, якщо у вашій школі повідомляється середня оцінка з предметів на сторінці Vulcan, програма тільки завантажує ці оцінки і не розраховує їх самостійно. Це можна змінити шляхом примусового розрахунку середньоЇ оцінки в налаштуваннях програми.\n\nСередні оцінки тільки за обраний семестр:\n1. Розрахунок середньозваженого числа для кожного предмета в даному семестрі\n2. Сумування розрахованих числ\n3. Розрахунок середнього арифметичного з сумованих чисел\n\nСереднє значення з обох семестрів:\n1. Обчислення середньозваженого числа для кожного предмета у 1 та 2 семестрі\n2. Обчислення середнього арифметичного з розрахованих середньозважених числ за 1 та 2 семестри для кожного предмета.\n3. Додавання розрахованих середніх\n4. Розрахунок середнього арифметичного підсумованих середніх значень\n\nСереднє значення оцінок за весь рік: \n1. Розрахунок середньозваженого числа за рік для кожного предмета. Підсумковий середній показник у 1-му семестрі не має значення.\n2. Сумування розрахованих середніх\n3. Обчислення середнього арифметичного з суммованих середніх @@ -157,6 +162,12 @@ Нові підсумкові оцінки Нові підсумкові оцінки + + Нова описова оцінка + Нових описових оцінок + Описових оцінок + Нові описові оцінки + Ви отримали %1$d нову оцінку Ви отримали %1$d нові оцінки @@ -175,6 +186,12 @@ Ви отримали %1$d нових підсумкових оцінок Ви отримали %1$d нових підсумкових оцінок + + Ви отримали %1$d описову оцінку + Ви отримали %1$d нові описові оцінки + Ви отримали %1$d нових описових оцінок + Ви отримали %1$d нових описових оцінок + Урок Аудиторія @@ -832,6 +849,9 @@ Авторизувати Для роботи програми нам потрібно підтвердити вашу особу. Будь ласка, введіть число PESEL <b>%1$s</b> студента в поле нижче Поки що пропустити + + Верифікація в процесі. Чекайте… + Верифікація завершена Немає з\'єднання з інтернетом Сталася помилка. Перевірте годинник пристрою @@ -841,6 +861,7 @@ UONET+ проводить технічне осблуговування, спробуйте пізніше Невідома помилка щоденника UONET+, спробуйте пізніше Невідома помилка програми, спробуйте пізніше + Необхідна перевірка Captcha Відбулася несподівана помилка Функція вимкнена вашою школою Функція недоступна в режимі Mobile API. Увійдіть в інший режим diff --git a/app/src/main/res/values-v31/styles.xml b/app/src/main/res/values-v31/styles.xml index bb47b22e..35880668 100644 --- a/app/src/main/res/values-v31/styles.xml +++ b/app/src/main/res/values-v31/styles.xml @@ -34,6 +34,9 @@ @color/material_dynamic_primary80 @color/timetable_canceled_light @color/timetable_change_light + @color/attendance_absence_light + @color/attendance_lateness_light + @color/colorError @color/colorDivider @color/material_dynamic_secondary90 diff --git a/app/src/main/res/values/api_hosts.xml b/app/src/main/res/values/api_hosts.xml index 522b6e11..9768329d 100644 --- a/app/src/main/res/values/api_hosts.xml +++ b/app/src/main/res/values/api_hosts.xml @@ -44,10 +44,10 @@ https://vulcan.net.pl/?login https://vulcan.net.pl/?login https://vulcan.net.pl/?email&customSuffix - https://fakelog.cf/?email + https://wulkanowy.net.pl/?email - Default + warszawa opole gdansk lublin @@ -66,7 +66,7 @@ gminaulanmajorat gminaozorkow gminalopiennikgorny - Default + saas1 powiatwulkanowy diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index aa58fa09..3c3fdb8e 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -7,4 +7,6 @@ + + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 87057c61..8ad27ad8 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -49,6 +49,12 @@ #ff8f00 #ffd54f + #d32f2f + #e57373 + + #cd2a01 + #f05d0e + #1f000000 #1fffffff diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bee0b782..652019fb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -14,6 +14,7 @@ Log viewer Debug Notification debug + Clear webview cookies Contributors Licenses Messages @@ -60,6 +61,7 @@ Invalid email Use the assigned login instead of email Use the assigned login or email in @%1$s + Invalid domain suffix Invalid symbol. If you cannot find it, please contact the school Don\'t make this up! If you cannot find it, please contact the school Student not found. Validate the symbol and the chosen variation of the UONET+ register @@ -107,6 +109,8 @@ Log in Session expired Session expired, log in again + Your account password has been changed. You need to log in to Wulkanowy again + Password changed Application support Do you like this app? Support its development by enabling non-invasive ads that you can disable at any time Enable ads @@ -127,6 +131,7 @@ Total points Final grade Predicted grade + Descriptive grade Calculated average How does Calculated Average work? The Calculated Average is the arithmetic average calculated from the subjects averages. It allows you to know the approximate final average. It is calculated in a way selected by the user in the application settings. It is recommended that you choose the appropriate option. This is because the calculation of school averages differs. Additionally, if your school reports the average of the subjects on the Vulcan page, the application downloads them and does not calculate these averages. This can be changed by forcing the calculation of the average in the application settings.\n\nAverage of grades only from selected semester:\n1. Calculating the weighted average for each subject in a given semester\n2.Adding calculated averages\n3. Calculation of the arithmetic average of the summed averages\n\nAverage of averages from both semesters:\n1.Calculating the weighted average for each subject in semester 1 and 2\n2. Calculating the arithmetic average of the calculated averages for semesters 1 and 2 for each subject.\n3. Adding calculated averages\n4. Calculation of the arithmetic average of the summed averages\n\nAverage of grades from the whole year:\n1. Calculating weighted average over the year for each subject. The final average in the 1st semester is irrelevant.\n2. Adding calculated averages\n3. Calculating the arithmetic average of summed averages @@ -162,6 +167,10 @@ New final grade New final grades + + New descriptive grade + New descriptive grades + You received %1$d grade You received %1$d grades @@ -174,6 +183,10 @@ You received %1$d final grade You received %1$d final grades + + You received %1$d descriptive grade + You received %1$d descriptive grades + @@ -832,15 +845,22 @@ Skip for now + + Verification is in progress. Wait… + Verified successfully + + No internet connection An error occurred. Check your device clock + This account is inactive. Try logging in again Connection to register failed. Servers can be overloaded. Please try again later Loading data failed. Please try again later Register password change required Maintenance underway UONET + register. Try again later Unknown UONET + register error. Try again later Unknown application error. Please try again later + Captcha verification required An unexpected error occurred Feature disabled by your school Feature not available. Login in a mode other than Mobile API diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index a0023dda..b5b02950 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -19,6 +19,8 @@ @color/colorSurface @color/timetable_canceled_light @color/timetable_change_light + @color/attendance_absence_light + @color/attendance_lateness_light @color/colorError @color/colorDivider @color/colorSwipeRefresh diff --git a/app/src/play/java/io/github/wulkanowy/ui/modules/settings/ads/AdsFragment.kt b/app/src/play/java/io/github/wulkanowy/ui/modules/settings/ads/AdsFragment.kt index ec6027e9..30b9e6b7 100644 --- a/app/src/play/java/io/github/wulkanowy/ui/modules/settings/ads/AdsFragment.kt +++ b/app/src/play/java/io/github/wulkanowy/ui/modules/settings/ads/AdsFragment.kt @@ -101,8 +101,16 @@ class AdsFragment : PreferenceFragmentCompat(), MainView.TitledView, AdsView { (activity as? BaseActivity<*, *>)?.showMessage(text) } - override fun showExpiredDialog() { - (activity as? BaseActivity<*, *>)?.showExpiredDialog() + override fun showExpiredCredentialsDialog() { + (activity as? BaseActivity<*, *>)?.showExpiredCredentialsDialog() + } + + override fun onCaptchaVerificationRequired(url: String?) { + (activity as? BaseActivity<*, *>)?.onCaptchaVerificationRequired(url) + } + + override fun showDecryptionFailedDialog() { + (activity as? BaseActivity<*, *>)?.showDecryptionFailedDialog() } override fun showChangePasswordSnackbar(redirectUrl: String) { diff --git a/app/src/test/java/io/github/wulkanowy/TestEnityCreator.kt b/app/src/test/java/io/github/wulkanowy/TestEnityCreator.kt index eac1389f..9f5d731b 100644 --- a/app/src/test/java/io/github/wulkanowy/TestEnityCreator.kt +++ b/app/src/test/java/io/github/wulkanowy/TestEnityCreator.kt @@ -42,6 +42,7 @@ fun getSemesterPojo(diaryId: Int, semesterId: Int, start: LocalDate, end: LocalD diaryName = "$semesterId", schoolYear = 1970, classId = 0, + className = "Ti", semesterNumber = semesterName, unitId = 1, start = start, diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/GradeRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/GradeRepositoryTest.kt index 5a1877cc..515b0d66 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/GradeRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/GradeRepositoryTest.kt @@ -2,6 +2,7 @@ package io.github.wulkanowy.data.repositories import io.github.wulkanowy.data.dataOrNull import io.github.wulkanowy.data.db.dao.GradeDao +import io.github.wulkanowy.data.db.dao.GradeDescriptiveDao import io.github.wulkanowy.data.db.dao.GradeSummaryDao import io.github.wulkanowy.data.errorOrNull import io.github.wulkanowy.data.mappers.mapToEntities @@ -42,6 +43,9 @@ class GradeRepositoryTest { @MockK private lateinit var gradeSummaryDb: GradeSummaryDao + @MockK + private lateinit var gradeDescriptiveDb: GradeDescriptiveDao + @MockK(relaxUnitFun = true) private lateinit var refreshHelper: AutoRefreshHelper @@ -56,7 +60,8 @@ class GradeRepositoryTest { MockKAnnotations.init(this) every { refreshHelper.shouldBeRefreshed(any()) } returns false - gradeRepository = GradeRepository(gradeDb, gradeSummaryDb, sdk, refreshHelper) + gradeRepository = + GradeRepository(gradeDb, gradeSummaryDb, gradeDescriptiveDb, sdk, refreshHelper) coEvery { gradeDb.deleteAll(any()) } just Runs coEvery { gradeDb.insertAll(any()) } returns listOf() @@ -68,6 +73,13 @@ class GradeRepositoryTest { ) coEvery { gradeSummaryDb.deleteAll(any()) } just Runs coEvery { gradeSummaryDb.insertAll(any()) } returns listOf() + + coEvery { gradeDescriptiveDb.loadAll(any(), any()) } returnsMany listOf( + flowOf(listOf()), + ) + + coEvery { gradeDescriptiveDb.deleteAll(any()) } just Runs + coEvery { gradeDescriptiveDb.insertAll(any()) } returns listOf() } @Test diff --git a/app/src/test/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProviderTest.kt b/app/src/test/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProviderTest.kt index 31ea3322..6a717f6f 100644 --- a/app/src/test/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProviderTest.kt +++ b/app/src/test/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProviderTest.kt @@ -1,12 +1,16 @@ package io.github.wulkanowy.ui.modules.grade -import io.github.wulkanowy.data.* +import io.github.wulkanowy.data.Resource +import io.github.wulkanowy.data.dataOrNull import io.github.wulkanowy.data.db.entities.Grade import io.github.wulkanowy.data.db.entities.GradeSummary import io.github.wulkanowy.data.db.entities.Student +import io.github.wulkanowy.data.errorOrNull import io.github.wulkanowy.data.repositories.GradeRepository import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.SemesterRepository +import io.github.wulkanowy.data.resourceFlow +import io.github.wulkanowy.data.toFirstResult import io.github.wulkanowy.getSemesterEntity import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.Status @@ -158,7 +162,9 @@ class GradeAverageProviderTest { semesters[2], true ) - } returns resourceFlow { noWeightGrades to noWeightGradesSummary } + } returns resourceFlow { + Triple(noWeightGrades, noWeightGradesSummary, emptyList()) + } val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage( @@ -186,7 +192,9 @@ class GradeAverageProviderTest { semesters[2], true ) - } returns resourceFlow { noWeightGrades to noWeightGradesArithmeticSummary } + } returns resourceFlow { + Triple(noWeightGrades, noWeightGradesArithmeticSummary, emptyList()) + } val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage( @@ -211,8 +219,24 @@ class GradeAverageProviderTest { coEvery { semesterRepository.getSemesters(student) } returns semesters coEvery { gradeRepository.getGrades(student, semesters[2], true) } returns flow { emit(Resource.Loading()) - emit(Resource.Intermediate(secondGradeWithModifier to secondSummariesWithModifier)) - emit(Resource.Success(secondGradeWithModifier to secondSummariesWithModifier)) + emit( + Resource.Intermediate( + Triple( + secondGradeWithModifier, + secondSummariesWithModifier, + emptyList() + ) + ) + ) + emit( + Resource.Success( + Triple( + secondGradeWithModifier, + secondSummariesWithModifier, + emptyList() + ) + ) + ) } val items = runBlocking { @@ -253,11 +277,27 @@ class GradeAverageProviderTest { coEvery { gradeRepository.getGrades(student, semesters[2], false) } returns flow { emit(Resource.Loading()) delay(1000) - emit(Resource.Success(secondGradeWithModifier to secondSummariesWithModifier)) + emit( + Resource.Success( + Triple( + secondGradeWithModifier, + secondSummariesWithModifier, + emptyList() + ) + ) + ) } coEvery { gradeRepository.getGrades(student, semesters[1], false) } returns flow { emit(Resource.Loading()) - emit(Resource.Success(secondGradeWithModifier to secondSummariesWithModifier)) + emit( + Resource.Success( + Triple( + secondGradeWithModifier, + secondSummariesWithModifier, + emptyList() + ) + ) + ) } val items = runBlocking { @@ -296,7 +336,13 @@ class GradeAverageProviderTest { semesters[1], false ) - } returns resourceFlow { secondGradeWithModifier to secondSummariesWithModifier } + } returns resourceFlow { + Triple( + secondGradeWithModifier, + secondSummariesWithModifier, + emptyList() + ) + } coEvery { gradeRepository.getGrades( student, @@ -304,8 +350,10 @@ class GradeAverageProviderTest { false ) } returns resourceFlow { - listOf(getGrade(semesters[2].semesterId, "Język polski", .0, .0, .0)) to listOf( - getSummary(semesters[2].semesterId, "Język polski", 2.5) + Triple( + listOf(getGrade(semesters[2].semesterId, "Język polski", .0, .0, .0)), + listOf(getSummary(semesters[2].semesterId, "Język polski", 2.5)), + emptyList() ) } @@ -332,7 +380,13 @@ class GradeAverageProviderTest { semesters[1], false ) - } returns resourceFlow { secondGradeWithModifier to secondSummariesWithModifier } + } returns resourceFlow { + Triple( + secondGradeWithModifier, + secondSummariesWithModifier, + emptyList() + ) + } coEvery { gradeRepository.getGrades( student, @@ -340,12 +394,14 @@ class GradeAverageProviderTest { false ) } returns resourceFlow { - emptyList() to listOf( - getSummary( - 24, - "Język polski", - .0 - ) + Triple( + emptyList(), listOf( + getSummary( + 24, + "Język polski", + .0 + ) + ), emptyList() ) } @@ -372,14 +428,22 @@ class GradeAverageProviderTest { semesters[1], true ) - } returns resourceFlow { emptyList() to emptyList() } + } returns resourceFlow { + Triple( + emptyList(), + emptyList(), + emptyList() + ) + } coEvery { gradeRepository.getGrades( student, semesters[2], true ) - } returns resourceFlow { emptyList() to emptyList() } + } returns resourceFlow { + Triple(emptyList(), emptyList(), emptyList()) + } val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage( @@ -404,7 +468,13 @@ class GradeAverageProviderTest { semesters[2], true ) - } returns resourceFlow { secondGradeWithModifier to secondSummariesWithModifier } + } returns resourceFlow { + Triple( + secondGradeWithModifier, + secondSummariesWithModifier, + emptyList() + ) + } val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage( @@ -438,7 +508,13 @@ class GradeAverageProviderTest { semesters[2], true ) - } returns resourceFlow { secondGradeWithModifier to secondSummariesWithModifier } + } returns resourceFlow { + Triple( + secondGradeWithModifier, + secondSummariesWithModifier, + emptyList() + ) + } val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage( @@ -472,7 +548,13 @@ class GradeAverageProviderTest { semesters[2], true ) - } returns resourceFlow { secondGradeWithModifier to secondSummariesWithModifier } + } returns resourceFlow { + Triple( + secondGradeWithModifier, + secondSummariesWithModifier, + emptyList() + ) + } val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage( @@ -506,7 +588,13 @@ class GradeAverageProviderTest { semesters[2], true ) - } returns resourceFlow { secondGradeWithModifier to secondSummariesWithModifier } + } returns resourceFlow { + Triple( + secondGradeWithModifier, + secondSummariesWithModifier, + emptyList() + ) + } val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage( @@ -534,7 +622,7 @@ class GradeAverageProviderTest { semesters[2], true ) - } returns resourceFlow { secondGrades to secondSummaries } + } returns resourceFlow { Triple(secondGrades, secondSummaries, emptyList()) } val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage( @@ -564,7 +652,7 @@ class GradeAverageProviderTest { semesters[2], true ) - } returns resourceFlow { secondGrades to secondSummaries } + } returns resourceFlow { Triple(secondGrades, secondSummaries, emptyList()) } val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage( @@ -594,7 +682,7 @@ class GradeAverageProviderTest { semesters[1], true ) - } returns resourceFlow { firstGrades to firstSummaries } + } returns resourceFlow { Triple(firstGrades, firstSummaries, emptyList()) } val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage( @@ -625,8 +713,8 @@ class GradeAverageProviderTest { coEvery { semesterRepository.getSemesters(student) } returns semesters coEvery { gradeRepository.getGrades(student, semesters[1], true) } returns flow { emit(Resource.Loading()) - emit(Resource.Intermediate(firstGrades to firstSummaries)) - emit(Resource.Success(firstGrades to firstSummaries)) + emit(Resource.Intermediate(Triple(firstGrades, firstSummaries, emptyList()))) + emit(Resource.Success(Triple(firstGrades, firstSummaries, emptyList()))) } val items = runBlocking { @@ -675,9 +763,11 @@ class GradeAverageProviderTest { true ) } returns resourceFlow { - firstGrades to listOf( - getSummary(22, "Matematyka", 3.0), - getSummary(22, "Fizyka", 3.5) + Triple( + firstGrades, listOf( + getSummary(22, "Matematyka", 3.0), + getSummary(22, "Fizyka", 3.5) + ), emptyList() ) } coEvery { @@ -687,9 +777,13 @@ class GradeAverageProviderTest { true ) } returns resourceFlow { - secondGrades to listOf( - getSummary(22, "Matematyka", 3.5), - getSummary(22, "Fizyka", 4.0) + Triple( + secondGrades, + listOf( + getSummary(22, "Matematyka", 3.5), + getSummary(22, "Fizyka", 4.0) + ), + emptyList() ) } @@ -723,17 +817,21 @@ class GradeAverageProviderTest { emit(Resource.Loading()) emit( Resource.Intermediate( - firstGrades to listOf( - getSummary(22, "Matematyka", 3.0), - getSummary(22, "Fizyka", 3.5) + Triple( + firstGrades, listOf( + getSummary(22, "Matematyka", 3.0), + getSummary(22, "Fizyka", 3.5) + ), emptyList() ) ) ) emit( Resource.Success( - firstGrades to listOf( - getSummary(22, "Matematyka", 3.0), - getSummary(22, "Fizyka", 3.5) + Triple( + firstGrades, listOf( + getSummary(22, "Matematyka", 3.0), + getSummary(22, "Fizyka", 3.5) + ), emptyList() ) ) ) @@ -742,17 +840,21 @@ class GradeAverageProviderTest { emit(Resource.Loading()) emit( Resource.Intermediate( - secondGrades to listOf( - getSummary(22, "Matematyka", 3.5), - getSummary(22, "Fizyka", 4.0) + Triple( + secondGrades, listOf( + getSummary(22, "Matematyka", 3.5), + getSummary(22, "Fizyka", 4.0) + ), emptyList() ) ) ) emit( Resource.Success( - secondGrades to listOf( - getSummary(22, "Matematyka", 3.5), - getSummary(22, "Fizyka", 4.0) + Triple( + secondGrades, listOf( + getSummary(22, "Matematyka", 3.5), + getSummary(22, "Fizyka", 4.0) + ), emptyList() ) ) ) @@ -803,7 +905,7 @@ class GradeAverageProviderTest { semesters[1], true ) - } returns resourceFlow { firstGrades to firstSummaries } + } returns resourceFlow { Triple(firstGrades, firstSummaries, emptyList()) } coEvery { gradeRepository.getGrades( student, @@ -811,9 +913,11 @@ class GradeAverageProviderTest { true ) } returns resourceFlow { - secondGrades to listOf( - getSummary(22, "Matematyka", 1.1), - getSummary(22, "Fizyka", 7.26) + Triple( + secondGrades, listOf( + getSummary(22, "Matematyka", 1.1), + getSummary(22, "Fizyka", 7.26) + ), emptyList() ) } @@ -850,9 +954,11 @@ class GradeAverageProviderTest { true ) } returns resourceFlow { - firstGrades to listOf( - getSummary(22, "Matematyka", .0), - getSummary(22, "Fizyka", .0) + Triple( + firstGrades, listOf( + getSummary(22, "Matematyka", .0), + getSummary(22, "Fizyka", .0) + ), emptyList() ) } coEvery { @@ -862,9 +968,11 @@ class GradeAverageProviderTest { true ) } returns resourceFlow { - secondGrades to listOf( - getSummary(22, "Matematyka", .0), - getSummary(22, "Fizyka", .0) + Triple( + secondGrades, listOf( + getSummary(22, "Matematyka", .0), + getSummary(22, "Fizyka", .0) + ), emptyList() ) } @@ -889,24 +997,28 @@ class GradeAverageProviderTest { coEvery { semesterRepository.getSemesters(student) } returns semesters coEvery { gradeRepository.getGrades(student, semesters[1], true) } returns flow { emit(Resource.Loading()) - emit(Resource.Intermediate(firstGrades to firstSummaries)) - emit(Resource.Success(firstGrades to firstSummaries)) + emit(Resource.Intermediate(Triple(firstGrades, firstSummaries, emptyList()))) + emit(Resource.Success(Triple(firstGrades, firstSummaries, emptyList()))) } coEvery { gradeRepository.getGrades(student, semesters[2], true) } returns flow { emit(Resource.Loading()) emit( Resource.Intermediate( - secondGrades to listOf( - getSummary(22, "Matematyka", 1.1), - getSummary(22, "Fizyka", 7.26) + Triple( + secondGrades, listOf( + getSummary(22, "Matematyka", 1.1), + getSummary(22, "Fizyka", 7.26) + ), emptyList() ) ) ) emit( Resource.Success( - secondGrades to listOf( - getSummary(22, "Matematyka", 1.1), - getSummary(22, "Fizyka", 7.26) + Triple( + secondGrades, listOf( + getSummary(22, "Matematyka", 1.1), + getSummary(22, "Fizyka", 7.26) + ), emptyList() ) ) ) @@ -958,14 +1070,14 @@ class GradeAverageProviderTest { semesters[1], true ) - } returns resourceFlow { firstGrades to emptyList() } + } returns resourceFlow { Triple(firstGrades, emptyList(), emptyList()) } coEvery { gradeRepository.getGrades( student, semesters[2], true ) - } returns resourceFlow { secondGrades to emptyList() } + } returns resourceFlow { Triple(secondGrades, emptyList(), emptyList()) } val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage( @@ -1000,14 +1112,14 @@ class GradeAverageProviderTest { semesters[1], true ) - } returns resourceFlow { firstGrades to emptyList() } + } returns resourceFlow { Triple(firstGrades, emptyList(), emptyList()) } coEvery { gradeRepository.getGrades( student, semesters[2], true ) - } returns resourceFlow { secondGrades to emptyList() } + } returns resourceFlow { Triple(secondGrades, emptyList(), emptyList()) } val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage( @@ -1043,8 +1155,10 @@ class GradeAverageProviderTest { true ) } returns resourceFlow { - firstGrades to listOf( - getSummary(22, "Matematyka", 4.0) + Triple( + firstGrades, listOf( + getSummary(22, "Matematyka", 4.0) + ), emptyList() ) } coEvery { @@ -1054,8 +1168,10 @@ class GradeAverageProviderTest { true ) } returns resourceFlow { - secondGrades to listOf( - getSummary(23, "Matematyka", 3.0) + Triple( + secondGrades, listOf( + getSummary(23, "Matematyka", 3.0) + ), emptyList() ) } @@ -1092,14 +1208,20 @@ class GradeAverageProviderTest { semesters[1], true ) - } returns resourceFlow { firstGrades to firstSummaries } + } returns resourceFlow { Triple(firstGrades, firstSummaries, emptyList()) } coEvery { gradeRepository.getGrades( student, semesters[2], true ) - } returns resourceFlow { secondGrades to secondSummaries.dropLast(1) } + } returns resourceFlow { + Triple( + secondGrades, + secondSummaries.dropLast(1), + emptyList() + ) + } val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage( @@ -1134,14 +1256,20 @@ class GradeAverageProviderTest { semesters[1], true ) - } returns resourceFlow { firstGrades to firstSummaries.dropLast(1) } + } returns resourceFlow { + Triple( + firstGrades, + firstSummaries.dropLast(1), + emptyList() + ) + } coEvery { gradeRepository.getGrades( student, semesters[2], true ) - } returns resourceFlow { secondGrades to secondSummaries } + } returns resourceFlow { Triple(secondGrades, secondSummaries, emptyList()) } val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage( @@ -1176,14 +1304,20 @@ class GradeAverageProviderTest { semesters[1], true ) - } returns resourceFlow { firstGrades to firstSummaries.dropLast(1) } + } returns resourceFlow { + Triple( + firstGrades, + firstSummaries.dropLast(1), + emptyList() + ) + } coEvery { gradeRepository.getGrades( student, semesters[2], true ) - } returns resourceFlow { secondGrades to secondSummaries } + } returns resourceFlow { Triple(secondGrades, secondSummaries, emptyList()) } val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage( @@ -1219,16 +1353,20 @@ class GradeAverageProviderTest { true ) } returns resourceFlow { - listOf( - getGrade(22, "Fizyka", 5.0, weight = 2.0), - getGrade(22, "Fizyka", 6.0, weight = 2.0), - getGrade(22, "Fizyka", 5.0, weight = 4.0), - getGrade(22, "Fizyka", 6.0, weight = 2.0), - getGrade(22, "Fizyka", 6.0, weight = 2.0), - getGrade(22, "Fizyka", 6.0, weight = 4.0), - getGrade(22, "Fizyka", 6.0, weight = 4.0), - getGrade(22, "Fizyka", 6.0, weight = 2.0) - ) to listOf(getSummary(semesterId = 22, subject = "Fizyka", average = .0)) + Triple( + listOf( + getGrade(22, "Fizyka", 5.0, weight = 2.0), + getGrade(22, "Fizyka", 6.0, weight = 2.0), + getGrade(22, "Fizyka", 5.0, weight = 4.0), + getGrade(22, "Fizyka", 6.0, weight = 2.0), + getGrade(22, "Fizyka", 6.0, weight = 2.0), + getGrade(22, "Fizyka", 6.0, weight = 4.0), + getGrade(22, "Fizyka", 6.0, weight = 4.0), + getGrade(22, "Fizyka", 6.0, weight = 2.0) + ), + listOf(getSummary(semesterId = 22, subject = "Fizyka", average = .0)), + emptyList() + ) } coEvery { gradeRepository.getGrades( @@ -1237,11 +1375,15 @@ class GradeAverageProviderTest { true ) } returns resourceFlow { - listOf( - getGrade(23, "Fizyka", 5.0, weight = 1.0), - getGrade(23, "Fizyka", 5.0, weight = 2.0), - getGrade(23, "Fizyka", 4.0, modifier = 0.3, weight = 2.0) - ) to listOf(getSummary(semesterId = 23, subject = "Fizyka", average = .0)) + Triple( + listOf( + getGrade(23, "Fizyka", 5.0, weight = 1.0), + getGrade(23, "Fizyka", 5.0, weight = 2.0), + getGrade(23, "Fizyka", 4.0, modifier = 0.3, weight = 2.0) + ), + listOf(getSummary(semesterId = 23, subject = "Fizyka", average = .0)), + emptyList() + ) } val items = runBlocking { @@ -1266,23 +1408,31 @@ class GradeAverageProviderTest { every { preferencesRepository.gradeAverageModeFlow } returns flowOf(GradeAverageMode.ALL_YEAR) coEvery { gradeRepository.getGrades(student, semesters[1], true) } returns resourceFlow { - listOf( - getGrade(22, "Fizyka", 5.0, weight = 2.0), - getGrade(22, "Fizyka", 6.0, weight = 2.0), - getGrade(22, "Fizyka", 5.0, weight = 4.0), - getGrade(22, "Fizyka", 6.0, weight = 2.0), - getGrade(22, "Fizyka", 6.0, weight = 2.0), - getGrade(22, "Fizyka", 6.0, weight = 4.0), - getGrade(22, "Fizyka", 6.0, weight = 4.0), - getGrade(22, "Fizyka", 6.0, weight = 2.0) - ) to listOf(getSummary(semesterId = 22, subject = "Fizyka", average = .0)) + Triple( + listOf( + getGrade(22, "Fizyka", 5.0, weight = 2.0), + getGrade(22, "Fizyka", 6.0, weight = 2.0), + getGrade(22, "Fizyka", 5.0, weight = 4.0), + getGrade(22, "Fizyka", 6.0, weight = 2.0), + getGrade(22, "Fizyka", 6.0, weight = 2.0), + getGrade(22, "Fizyka", 6.0, weight = 4.0), + getGrade(22, "Fizyka", 6.0, weight = 4.0), + getGrade(22, "Fizyka", 6.0, weight = 2.0) + ), + listOf(getSummary(semesterId = 22, subject = "Fizyka", average = .0)), + emptyList() + ) } coEvery { gradeRepository.getGrades(student, semesters[2], true) } returns resourceFlow { - listOf( - getGrade(23, "Fizyka", 5.0, weight = 1.0), - getGrade(23, "Fizyka", 5.0, weight = 2.0), - getGrade(23, "Fizyka", 4.0, modifier = 0.3, weight = 2.0) - ) to listOf(getSummary(semesterId = 23, subject = "Fizyka", average = .0)) + Triple( + listOf( + getGrade(23, "Fizyka", 5.0, weight = 1.0), + getGrade(23, "Fizyka", 5.0, weight = 2.0), + getGrade(23, "Fizyka", 4.0, modifier = 0.3, weight = 2.0) + ), + listOf(getSummary(semesterId = 23, subject = "Fizyka", average = .0)), + emptyList() + ) } val items = runBlocking { @@ -1313,23 +1463,31 @@ class GradeAverageProviderTest { coEvery { semesterRepository.getSemesters(student) } returns semesters coEvery { gradeRepository.getGrades(student, semesters[1], true) } returns resourceFlow { - listOf( - getGrade(22, "Fizyka", 5.0, weight = 2.0), - getGrade(22, "Fizyka", 6.0, weight = 2.0), - getGrade(22, "Fizyka", 5.0, weight = 4.0), - getGrade(22, "Fizyka", 6.0, weight = 2.0), - getGrade(22, "Fizyka", 6.0, weight = 2.0), - getGrade(22, "Fizyka", 6.0, weight = 4.0), - getGrade(22, "Fizyka", 6.0, weight = 4.0), - getGrade(22, "Fizyka", 6.0, weight = 2.0) - ) to listOf(getSummary(semesterId = 22, subject = "Fizyka", average = .0)) + Triple( + listOf( + getGrade(22, "Fizyka", 5.0, weight = 2.0), + getGrade(22, "Fizyka", 6.0, weight = 2.0), + getGrade(22, "Fizyka", 5.0, weight = 4.0), + getGrade(22, "Fizyka", 6.0, weight = 2.0), + getGrade(22, "Fizyka", 6.0, weight = 2.0), + getGrade(22, "Fizyka", 6.0, weight = 4.0), + getGrade(22, "Fizyka", 6.0, weight = 4.0), + getGrade(22, "Fizyka", 6.0, weight = 2.0) + ), + listOf(getSummary(semesterId = 22, subject = "Fizyka", average = .0)), + emptyList() + ) } coEvery { gradeRepository.getGrades(student, semesters[2], true) } returns resourceFlow { - listOf( - getGrade(23, "Fizyka", 5.0, weight = 1.0), - getGrade(23, "Fizyka", 5.0, weight = 2.0), - getGrade(23, "Fizyka", 4.0, modifier = 0.33, weight = 2.0) - ) to listOf(getSummary(semesterId = 23, subject = "Fizyka", average = .0)) + Triple( + listOf( + getGrade(23, "Fizyka", 5.0, weight = 1.0), + getGrade(23, "Fizyka", 5.0, weight = 2.0), + getGrade(23, "Fizyka", 4.0, modifier = 0.33, weight = 2.0) + ), + listOf(getSummary(semesterId = 23, subject = "Fizyka", average = .0)), + emptyList() + ) } val items = runBlocking { @@ -1366,16 +1524,20 @@ class GradeAverageProviderTest { true ) } returns resourceFlow { - listOf( - getGrade(22, "Fizyka", 5.0, weight = 2.0), - getGrade(22, "Fizyka", 6.0, weight = 2.0), - getGrade(22, "Fizyka", 5.0, weight = 4.0), - getGrade(22, "Fizyka", 6.0, weight = 2.0), - getGrade(22, "Fizyka", 6.0, weight = 2.0), - getGrade(22, "Fizyka", 6.0, weight = 4.0), - getGrade(22, "Fizyka", 6.0, weight = 4.0), - getGrade(22, "Fizyka", 6.0, weight = 2.0) - ) to listOf(getSummary(semesterId = 22, subject = "Fizyka", average = .0)) + Triple( + listOf( + getGrade(22, "Fizyka", 5.0, weight = 2.0), + getGrade(22, "Fizyka", 6.0, weight = 2.0), + getGrade(22, "Fizyka", 5.0, weight = 4.0), + getGrade(22, "Fizyka", 6.0, weight = 2.0), + getGrade(22, "Fizyka", 6.0, weight = 2.0), + getGrade(22, "Fizyka", 6.0, weight = 4.0), + getGrade(22, "Fizyka", 6.0, weight = 4.0), + getGrade(22, "Fizyka", 6.0, weight = 2.0) + ), + listOf(getSummary(semesterId = 22, subject = "Fizyka", average = .0)), + emptyList() + ) } coEvery { gradeRepository.getGrades( @@ -1384,11 +1546,15 @@ class GradeAverageProviderTest { true ) } returns resourceFlow { - listOf( - getGrade(23, "Fizyka", 5.0, weight = 1.0), - getGrade(23, "Fizyka", 5.0, weight = 2.0), - getGrade(23, "Fizyka", 4.0, modifier = 0.33, weight = 2.0) - ) to listOf(getSummary(semesterId = 23, subject = "Fizyka", average = .0)) + Triple( + listOf( + getGrade(23, "Fizyka", 5.0, weight = 1.0), + getGrade(23, "Fizyka", 5.0, weight = 2.0), + getGrade(23, "Fizyka", 4.0, modifier = 0.33, weight = 2.0) + ), + listOf(getSummary(semesterId = 23, subject = "Fizyka", average = .0)), + emptyList() + ) } val items = runBlocking { @@ -1413,9 +1579,9 @@ class GradeAverageProviderTest { every { preferencesRepository.isOptionalArithmeticAverageFlow } returns flowOf(false) coEvery { gradeRepository.getGrades(student, semesters[1], true) } returns - resourceFlow { firstGrades to firstSummaries } + resourceFlow { Triple(firstGrades, firstSummaries, emptyList()) } coEvery { gradeRepository.getGrades(student, semesters[2], true) } returns - resourceFlow { listOf() to firstSummaries } + resourceFlow { Triple(listOf(), firstSummaries, emptyList()) } val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage( diff --git a/app/src/test/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenterTest.kt b/app/src/test/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenterTest.kt index fad6436d..34965f00 100644 --- a/app/src/test/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenterTest.kt +++ b/app/src/test/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenterTest.kt @@ -58,7 +58,7 @@ class LoginStudentSelectPresenterTest { login = "", password = "", baseUrl = "", - symbol = null, + defaultSymbol = "warszawa", domainSuffix = "", ) diff --git a/build.gradle b/build.gradle index 9c498b60..8a65ae89 100644 --- a/build.gradle +++ b/build.gradle @@ -16,10 +16,12 @@ buildscript { classpath "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin:$kotlin_version-1.0.15" // NOTE: Version downgraded due to https://github.com/appwrite/sdk-for-android/issues/42. - classpath 'com.android.tools.build:gradle:8.1.1' - + //classpath 'com.android.tools.build:gradle:8.1.1' + + classpath "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin:$kotlin_version-1.0.16" + classpath 'com.android.tools.build:gradle:8.2.2' classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" - classpath 'com.google.gms:google-services:4.4.0' + classpath 'com.google.gms:google-services:4.4.1' classpath 'com.huawei.agconnect:agcp:1.9.1.303' classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.9' classpath "com.github.triplet.gradle:play-publisher:3.8.4" @@ -32,6 +34,7 @@ buildscript { allprojects { repositories { + mavenLocal() mavenCentral() google() maven { url "https://jitpack.io" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 033e24c4..7f93135c 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 8838ba97..e6aba251 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-all.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index fcb6fca1..1aa94a42 100755 --- a/gradlew +++ b/gradlew @@ -83,7 +83,8 @@ done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -144,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac @@ -152,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -201,11 +202,11 @@ fi # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \