diff --git a/.github/workflows/deploy-store.yml b/.github/workflows/deploy-store.yml index e7ed6b49..068b9c10 100644 --- a/.github/workflows/deploy-store.yml +++ b/.github/workflows/deploy-store.yml @@ -28,15 +28,14 @@ jobs: SERVICES_ENCRYPT_KEY: ${{ secrets.SERVICES_ENCRYPT_KEY }} run: | gpg --yes --batch --passphrase=$SERVICES_ENCRYPT_KEY ./app/src/release/google-services.json.gpg - gpg --yes --batch --passphrase=$ENCRYPT_KEY ./app/key.p12.gpg gpg --yes --batch --passphrase=$ENCRYPT_KEY ./app/upload-key.jks.gpg - name: Upload apk to google play env: + PLAY_STORE_PASSWORD: ${{ secrets.PLAY_STORE_PASSWORD }} PLAY_KEY_ALIAS: ${{ secrets.PLAY_KEY_ALIAS }} PLAY_KEY_PASSWORD: ${{ secrets.PLAY_KEY_PASSWORD }} - PLAY_SERVICE_ACCOUNT_EMAIL: ${{ secrets.PLAY_SERVICE_ACCOUNT_EMAIL }} - PLAY_STORE_PASSWORD: ${{ secrets.PLAY_STORE_PASSWORD }} - run: ./gradlew publishPlayRelease -PenableFirebase --stacktrace; + ANDROID_PUBLISHER_CREDENTIALS: ${{ secrets.ANDROID_PUBLISHER_CREDENTIALS }} + run: ./gradlew publishPlayReleaseApps -PenableFirebase --stacktrace; deploy-app-gallery: name: Deploy to AppGallery @@ -60,7 +59,6 @@ jobs: SERVICES_ENCRYPT_KEY: ${{ secrets.SERVICES_ENCRYPT_KEY }} run: | gpg --yes --batch --passphrase=$SERVICES_ENCRYPT_KEY ./app/src/release/agconnect-services.json.gpg - gpg --yes --batch --passphrase=$ENCRYPT_KEY ./app/key.p12.gpg gpg --yes --batch --passphrase=$ENCRYPT_KEY ./app/upload-key.jks.gpg - name: Prepare credentials env: @@ -68,7 +66,7 @@ jobs: run: echo $AGC_CREDENTIALS > ./app/src/release/agconnect-credentials.json - name: Build and publish HMS version env: + PLAY_STORE_PASSWORD: ${{ secrets.PLAY_STORE_PASSWORD }} PLAY_KEY_ALIAS: ${{ secrets.PLAY_KEY_ALIAS }} PLAY_KEY_PASSWORD: ${{ secrets.PLAY_KEY_PASSWORD }} - PLAY_STORE_PASSWORD: ${{ secrets.PLAY_STORE_PASSWORD }} - run: ./gradlew assembleHmsRelease --stacktrace && ./gradlew publishHuaweiAppGalleryHmsRelease --stacktrace + run: ./gradlew bundleHmsRelease --stacktrace && ./gradlew publishHuaweiAppGalleryHmsRelease --stacktrace diff --git a/LICENSE b/LICENSE index 5dd9cacf..2fb96cee 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2019 Wulkanowy + Copyright 2021 Wulkanowy Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/app/build.gradle b/app/build.gradle index 695dc639..214eee79 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,5 +1,6 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' +apply plugin: 'kotlinx-serialization' apply plugin: 'kotlin-parcelize' apply plugin: 'kotlin-kapt' apply plugin: 'dagger.hilt.android.plugin' @@ -14,23 +15,22 @@ apply from: 'sonarqube.gradle' apply from: 'hooks.gradle' android { - compileSdkVersion 30 + compileSdkVersion 31 defaultConfig { applicationId "io.github.wulkanowy" testApplicationId "io.github.tests.wulkanowy" minSdkVersion 21 - targetSdkVersion 30 - versionCode 94 - versionName "1.2.1" + targetSdkVersion 31 + versionCode 98 + versionName "1.4.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - vectorDrawables.useSupportLibrary = true resValue "string", "app_name", "Wulkanowy" - buildConfigField "long", "BUILD_TIMESTAMP", String.valueOf(System.currentTimeMillis()) manifestPlaceholders = [ - firebase_enabled: project.hasProperty("enableFirebase") + firebase_enabled: project.hasProperty("enableFirebase"), + admob_project_id: "" ] javaCompileOptions { annotationProcessorOptions { @@ -40,6 +40,14 @@ android { ] } } + + buildConfigField "String", "SINGLE_SUPPORT_AD_ID", "null" + + if (System.env.SET_BUILD_TIMESTAMP) { + buildConfigField "long", "BUILD_TIMESTAMP", String.valueOf(System.currentTimeMillis()) + } else { + buildConfigField "long", "BUILD_TIMESTAMP", "1486235849000" + } } sourceSets { @@ -62,12 +70,14 @@ android { shrinkResources true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' signingConfig signingConfigs.release + buildConfigField "String", "MESSAGES_BASE_URL", "\"https://messages.wulkanowy.net.pl\"" } debug { - resValue "string", "app_name", "Wulkanowy DEV " + defaultConfig.versionCode + resValue "string", "app_name", "Wulkanowy DEV" applicationIdSuffix ".dev" versionNameSuffix "-dev" ext.enableCrashlytics = project.hasProperty("enableFirebase") + buildConfigField "String", "MESSAGES_BASE_URL", "\"https://messages.wulkanowy.net.pl\"" } } @@ -76,30 +86,38 @@ android { productFlavors { hms { dimension "platform" - manifestPlaceholders = [ - install_channel: "AppGallery" - ] + manifestPlaceholders = [install_channel: "AppGallery"] } play { dimension "platform" manifestPlaceholders = [ - install_channel: "Google Play" + install_channel : "Google Play", + admob_project_id: System.getenv("ADMOB_PROJECT_ID") ?: "ca-app-pub-3940256099942544~3347511713" ] + buildConfigField "String", "SINGLE_SUPPORT_AD_ID", "\"${System.getenv("SINGLE_SUPPORT_AD_ID") ?: "ca-app-pub-3940256099942544/5354046379"}\"" } fdroid { dimension "platform" - manifestPlaceholders = [ - install_channel: "F-Droid" - ] + manifestPlaceholders = [install_channel: "F-Droid"] } } + playConfigs { + play { enabled.set(true) } + } + buildFeatures { viewBinding true } + bundle { + language { + enableSplit = false + } + } + testOptions.unitTests { includeAndroidResources = true } @@ -130,61 +148,61 @@ kapt { } play { - serviceAccountEmail = System.getenv("PLAY_SERVICE_ACCOUNT_EMAIL") ?: "jan@fakelog.cf" - serviceAccountCredentials = file('key.p12') defaultToAppBundles = false - track = 'production' - updatePriority = 3 + track = 'beta' + updatePriority = 1 + enabled.set(false) } huaweiPublish { instances { hmsRelease { credentialsPath = "$rootDir/app/src/release/agconnect-credentials.json" - buildFormat = "apk" + buildFormat = "aab" deployType = "draft" } } } ext { - work_manager = "2.5.0" + work_manager = "2.7.0" android_hilt = "1.0.0" room = "2.3.0" chucker = "3.5.2" mockk = "1.12.0" - moshi = "1.12.0" + coroutines = "1.5.2" } dependencies { - implementation "io.github.wulkanowy:sdk:1.2.1" + implementation "io.github.wulkanowy:sdk:1.4.0" coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2" + implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.1" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines" - implementation "androidx.core:core-ktx:1.6.0" - implementation "androidx.activity:activity-ktx:1.3.1" - implementation "androidx.appcompat:appcompat:1.3.1" - implementation "androidx.appcompat:appcompat-resources:1.3.1" - implementation "androidx.fragment:fragment-ktx:1.3.6" - implementation "androidx.annotation:annotation:1.2.0" + implementation "androidx.core:core-ktx:1.7.0" + implementation 'androidx.core:core-splashscreen:1.0.0-alpha02' + implementation "androidx.activity:activity-ktx:1.4.0" + implementation "androidx.appcompat:appcompat:1.4.0-rc01" + implementation "androidx.fragment:fragment-ktx:1.4.0-rc01" + implementation "androidx.annotation:annotation:1.3.0" implementation "androidx.preference:preference-ktx:1.1.1" implementation "androidx.recyclerview:recyclerview:1.2.1" implementation "androidx.viewpager:viewpager:1.0.0" implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" - implementation "androidx.constraintlayout:constraintlayout:2.1.0" + implementation "androidx.constraintlayout:constraintlayout:2.1.1" implementation "androidx.coordinatorlayout:coordinatorlayout:1.1.0" implementation "com.google.android.material:material:1.4.0" - implementation "com.github.wulkanowy:material-chips-input:2.2.0" + implementation "com.github.wulkanowy:material-chips-input:2.3.1" implementation "com.github.PhilJay:MPAndroidChart:v3.1.0" implementation 'com.github.lopspower:CircularImageView:4.2.0' implementation "androidx.work:work-runtime-ktx:$work_manager" playImplementation "androidx.work:work-gcm:$work_manager" - implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.1" + implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.0" implementation "androidx.room:room-runtime:$room" implementation "androidx.room:room-ktx:$room" @@ -198,40 +216,41 @@ dependencies { implementation 'com.github.ncapdevi:FragNav:3.3.0' implementation "com.github.YarikSOffice:lingver:1.3.0" - implementation "com.squareup.moshi:moshi:$moshi" - implementation "com.squareup.moshi:moshi-adapters:$moshi" - kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi" + implementation 'com.squareup.retrofit2:retrofit:2.9.0' + implementation "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:0.8.0" + implementation "com.squareup.okhttp3:logging-interceptor:4.9.2" implementation "com.jakewharton.timber:timber:5.0.1" implementation "at.favre.lib:slf4j-timber:1.0.1" - implementation 'com.github.bastienpaulfr:Treessence:1.0.4' + implementation 'com.github.bastienpaulfr:Treessence:1.0.5' implementation "com.mikepenz:aboutlibraries-core:$about_libraries" - implementation "io.coil-kt:coil:1.3.2" + implementation "io.coil-kt:coil:1.4.0" implementation "io.github.wulkanowy:AppKillerManager:3.0.0" implementation 'me.xdrop:fuzzywuzzy:1.3.1' implementation 'com.fredporciuncula:flow-preferences:1.5.0' - playImplementation platform('com.google.firebase:firebase-bom:28.4.0') + playImplementation platform('com.google.firebase:firebase-bom:29.0.0') playImplementation 'com.google.firebase:firebase-analytics-ktx' playImplementation 'com.google.firebase:firebase-messaging:' playImplementation 'com.google.firebase:firebase-crashlytics:' - playImplementation 'com.google.android.play:core:1.10.1' + playImplementation 'com.google.android.play:core:1.10.2' playImplementation 'com.google.android.play:core-ktx:1.8.1' + playImplementation 'com.google.android.gms:play-services-ads:20.4.0' - hmsImplementation 'com.huawei.hms:hianalytics:6.2.0.301' - hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.6.0.300' + hmsImplementation 'com.huawei.hms:hianalytics:6.3.0.303' + hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.6.1.300' releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker" debugImplementation "com.github.ChuckerTeam.Chucker:library:$chucker" - debugImplementation 'com.github.amitshekhariitbhu.Android-Debug-Database:debug-db:v1.0.6' + debugImplementation 'com.github.amitshekhariitbhu.Android-Debug-Database:debug-db:1.0.6' testImplementation "junit:junit:4.13.2" testImplementation "io.mockk:mockk:$mockk" - testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.2' + testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines" testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version" - testImplementation 'org.robolectric:robolectric:4.6.1' + testImplementation 'org.robolectric:robolectric:4.7' testImplementation "androidx.test:runner:1.4.0" testImplementation "androidx.test.ext:junit:1.1.3" testImplementation "androidx.test:core:1.4.0" diff --git a/app/key.p12.gpg b/app/key.p12.gpg deleted file mode 100644 index e9b6d06e..00000000 Binary files a/app/key.p12.gpg and /dev/null differ diff --git a/app/schemas/io.github.wulkanowy.data.db.AppDatabase/40.json b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/40.json new file mode 100644 index 00000000..362c7f0e --- /dev/null +++ b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/40.json @@ -0,0 +1,2316 @@ +{ + "formatVersion": 1, + "database": { + "version": 40, + "identityHash": "e2fba6244951713b4e9b217adc5d1a23", + "entities": [ + { + "tableName": "Students", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`scrapper_base_url` TEXT NOT NULL, `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": "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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_Students_email_symbol_student_id_school_id_class_id", + "unique": true, + "columnNames": [ + "email", + "symbol", + "student_id", + "school_id", + "class_id" + ], + "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, `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": "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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_Semesters_student_id_diary_id_semester_id", + "unique": true, + "columnNames": [ + "student_id", + "diary_id", + "semester_id" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Semesters_student_id_diary_id_semester_id` ON `${TABLE_NAME}` (`student_id`, `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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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)", + "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 + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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)", + "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 + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Messages", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `real_id` INTEGER NOT NULL, `message_id` INTEGER NOT NULL, `sender_name` TEXT NOT NULL, `sender_id` INTEGER NOT NULL, `recipient_name` TEXT NOT NULL, `subject` TEXT NOT NULL, `date` INTEGER NOT NULL, `folder_id` INTEGER NOT NULL, `unread` INTEGER NOT NULL, `removed` INTEGER NOT NULL, `has_attachments` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL, `unread_by` INTEGER NOT NULL, `read_by` INTEGER NOT NULL, `content` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "realId", + "columnName": "real_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "messageId", + "columnName": "message_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sender", + "columnName": "sender_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "senderId", + "columnName": "sender_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "recipient", + "columnName": "recipient_name", + "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": "removed", + "columnName": "removed", + "affinity": "INTEGER", + "notNull": true + }, + { + "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": "unreadBy", + "columnName": "unread_by", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "readBy", + "columnName": "read_by", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MessageAttachments", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`real_id` INTEGER NOT NULL, `message_id` INTEGER NOT NULL, `one_drive_id` TEXT NOT NULL, `url` TEXT NOT NULL, `filename` TEXT NOT NULL, PRIMARY KEY(`real_id`))", + "fields": [ + { + "fieldPath": "realId", + "columnName": "real_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "messageId", + "columnName": "message_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "oneDriveId", + "columnName": "one_drive_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "filename", + "columnName": "filename", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "real_id" + ], + "autoGenerate": false + }, + "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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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)", + "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 + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ReportingUnits", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `real_id` INTEGER NOT NULL, `short` TEXT NOT NULL, `sender_id` INTEGER NOT NULL, `sender_name` TEXT NOT NULL, `roles` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unitId", + "columnName": "real_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "shortName", + "columnName": "short", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "senderId", + "columnName": "sender_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "senderName", + "columnName": "sender_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "roles", + "columnName": "roles", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Recipients", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `real_id` TEXT NOT NULL, `name` TEXT NOT NULL, `real_name` TEXT NOT NULL, `login_id` INTEGER NOT NULL, `unit_id` INTEGER NOT NULL, `role` INTEGER NOT NULL, `hash` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "realId", + "columnName": "real_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "realName", + "columnName": "real_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "loginId", + "columnName": "login_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unitId", + "columnName": "unit_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "role", + "columnName": "role", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hash", + "columnName": "hash", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MobileDevices", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_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": "student_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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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)", + "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 + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "SchoolAnnouncements", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_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": "studentId", + "columnName": "student_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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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, `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": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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, 'e2fba6244951713b4e9b217adc5d1a23')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.github.wulkanowy.data.db.AppDatabase/41.json b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/41.json new file mode 100644 index 00000000..9d008060 --- /dev/null +++ b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/41.json @@ -0,0 +1,2322 @@ +{ + "formatVersion": 1, + "database": { + "version": 41, + "identityHash": "d9ce44a78495a358606612bd91603c0f", + "entities": [ + { + "tableName": "Students", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`scrapper_base_url` TEXT NOT NULL, `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": "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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_Students_email_symbol_student_id_school_id_class_id", + "unique": true, + "columnNames": [ + "email", + "symbol", + "student_id", + "school_id", + "class_id" + ], + "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, `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": "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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_Semesters_student_id_diary_id_semester_id", + "unique": true, + "columnNames": [ + "student_id", + "diary_id", + "semester_id" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Semesters_student_id_diary_id_semester_id` ON `${TABLE_NAME}` (`student_id`, `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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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)", + "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 + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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)", + "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 + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Messages", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `real_id` INTEGER NOT NULL, `message_id` INTEGER NOT NULL, `sender_name` TEXT NOT NULL, `sender_id` INTEGER NOT NULL, `recipient_name` TEXT NOT NULL, `subject` TEXT NOT NULL, `date` INTEGER NOT NULL, `folder_id` INTEGER NOT NULL, `unread` INTEGER NOT NULL, `removed` INTEGER NOT NULL, `has_attachments` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL, `unread_by` INTEGER NOT NULL, `read_by` INTEGER NOT NULL, `content` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "realId", + "columnName": "real_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "messageId", + "columnName": "message_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sender", + "columnName": "sender_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "senderId", + "columnName": "sender_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "recipient", + "columnName": "recipient_name", + "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": "removed", + "columnName": "removed", + "affinity": "INTEGER", + "notNull": true + }, + { + "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": "unreadBy", + "columnName": "unread_by", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "readBy", + "columnName": "read_by", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MessageAttachments", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`real_id` INTEGER NOT NULL, `message_id` INTEGER NOT NULL, `one_drive_id` TEXT NOT NULL, `url` TEXT NOT NULL, `filename` TEXT NOT NULL, PRIMARY KEY(`real_id`))", + "fields": [ + { + "fieldPath": "realId", + "columnName": "real_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "messageId", + "columnName": "message_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "oneDriveId", + "columnName": "one_drive_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "filename", + "columnName": "filename", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "real_id" + ], + "autoGenerate": false + }, + "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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ReportingUnits", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `real_id` INTEGER NOT NULL, `short` TEXT NOT NULL, `sender_id` INTEGER NOT NULL, `sender_name` TEXT NOT NULL, `roles` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unitId", + "columnName": "real_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "shortName", + "columnName": "short", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "senderId", + "columnName": "sender_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "senderName", + "columnName": "sender_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "roles", + "columnName": "roles", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Recipients", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `real_id` TEXT NOT NULL, `name` TEXT NOT NULL, `real_name` TEXT NOT NULL, `login_id` INTEGER NOT NULL, `unit_id` INTEGER NOT NULL, `role` INTEGER NOT NULL, `hash` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "realId", + "columnName": "real_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "realName", + "columnName": "real_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "loginId", + "columnName": "login_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unitId", + "columnName": "unit_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "role", + "columnName": "role", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hash", + "columnName": "hash", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MobileDevices", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_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": "student_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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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)", + "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 + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "SchoolAnnouncements", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_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": "studentId", + "columnName": "student_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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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, `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": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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, 'd9ce44a78495a358606612bd91603c0f')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.github.wulkanowy.data.db.AppDatabase/42.json b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/42.json new file mode 100644 index 00000000..a5faa57b --- /dev/null +++ b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/42.json @@ -0,0 +1,2396 @@ +{ + "formatVersion": 1, + "database": { + "version": 42, + "identityHash": "5c8b7f9409294ecdebf9f74a44f8e883", + "entities": [ + { + "tableName": "Students", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`scrapper_base_url` TEXT NOT NULL, `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": "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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_Students_email_symbol_student_id_school_id_class_id", + "unique": true, + "columnNames": [ + "email", + "symbol", + "student_id", + "school_id", + "class_id" + ], + "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, `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": "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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_Semesters_student_id_diary_id_semester_id", + "unique": true, + "columnNames": [ + "student_id", + "diary_id", + "semester_id" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Semesters_student_id_diary_id_semester_id` ON `${TABLE_NAME}` (`student_id`, `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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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)", + "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 + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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)", + "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 + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Messages", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `real_id` INTEGER NOT NULL, `message_id` INTEGER NOT NULL, `sender_name` TEXT NOT NULL, `sender_id` INTEGER NOT NULL, `recipient_name` TEXT NOT NULL, `subject` TEXT NOT NULL, `date` INTEGER NOT NULL, `folder_id` INTEGER NOT NULL, `unread` INTEGER NOT NULL, `removed` INTEGER NOT NULL, `has_attachments` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL, `unread_by` INTEGER NOT NULL, `read_by` INTEGER NOT NULL, `content` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "realId", + "columnName": "real_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "messageId", + "columnName": "message_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sender", + "columnName": "sender_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "senderId", + "columnName": "sender_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "recipient", + "columnName": "recipient_name", + "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": "removed", + "columnName": "removed", + "affinity": "INTEGER", + "notNull": true + }, + { + "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": "unreadBy", + "columnName": "unread_by", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "readBy", + "columnName": "read_by", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MessageAttachments", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`real_id` INTEGER NOT NULL, `message_id` INTEGER NOT NULL, `one_drive_id` TEXT NOT NULL, `url` TEXT NOT NULL, `filename` TEXT NOT NULL, PRIMARY KEY(`real_id`))", + "fields": [ + { + "fieldPath": "realId", + "columnName": "real_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "messageId", + "columnName": "message_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "oneDriveId", + "columnName": "one_drive_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "filename", + "columnName": "filename", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "real_id" + ], + "autoGenerate": false + }, + "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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ReportingUnits", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `real_id` INTEGER NOT NULL, `short` TEXT NOT NULL, `sender_id` INTEGER NOT NULL, `sender_name` TEXT NOT NULL, `roles` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unitId", + "columnName": "real_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "shortName", + "columnName": "short", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "senderId", + "columnName": "sender_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "senderName", + "columnName": "sender_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "roles", + "columnName": "roles", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Recipients", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `real_id` TEXT NOT NULL, `name` TEXT NOT NULL, `real_name` TEXT NOT NULL, `login_id` INTEGER NOT NULL, `unit_id` INTEGER NOT NULL, `role` INTEGER NOT NULL, `hash` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "realId", + "columnName": "real_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "realName", + "columnName": "real_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "loginId", + "columnName": "login_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unitId", + "columnName": "unit_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "role", + "columnName": "role", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hash", + "columnName": "hash", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MobileDevices", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_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": "student_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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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)", + "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 + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "SchoolAnnouncements", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_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": "studentId", + "columnName": "student_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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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, `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": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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, `type` TEXT NOT NULL, 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": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "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, '5c8b7f9409294ecdebf9f74a44f8e883')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.github.wulkanowy.data.db.AppDatabase/43.json b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/43.json new file mode 100644 index 00000000..22c0d812 --- /dev/null +++ b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/43.json @@ -0,0 +1,2408 @@ +{ + "formatVersion": 1, + "database": { + "version": 43, + "identityHash": "66946510bb620ae82686a5a1a31aba18", + "entities": [ + { + "tableName": "Students", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`scrapper_base_url` TEXT NOT NULL, `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": "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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_Students_email_symbol_student_id_school_id_class_id", + "unique": true, + "columnNames": [ + "email", + "symbol", + "student_id", + "school_id", + "class_id" + ], + "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, `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": "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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_Semesters_student_id_diary_id_semester_id", + "unique": true, + "columnNames": [ + "student_id", + "diary_id", + "semester_id" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Semesters_student_id_diary_id_semester_id` ON `${TABLE_NAME}` (`student_id`, `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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Messages", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `real_id` INTEGER NOT NULL, `message_id` INTEGER NOT NULL, `sender_name` TEXT NOT NULL, `sender_id` INTEGER NOT NULL, `recipient_name` TEXT NOT NULL, `subject` TEXT NOT NULL, `date` INTEGER NOT NULL, `folder_id` INTEGER NOT NULL, `unread` INTEGER NOT NULL, `removed` INTEGER NOT NULL, `has_attachments` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL, `unread_by` INTEGER NOT NULL, `read_by` INTEGER NOT NULL, `content` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "realId", + "columnName": "real_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "messageId", + "columnName": "message_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sender", + "columnName": "sender_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "senderId", + "columnName": "sender_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "recipient", + "columnName": "recipient_name", + "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": "removed", + "columnName": "removed", + "affinity": "INTEGER", + "notNull": true + }, + { + "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": "unreadBy", + "columnName": "unread_by", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "readBy", + "columnName": "read_by", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MessageAttachments", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`real_id` INTEGER NOT NULL, `message_id` INTEGER NOT NULL, `one_drive_id` TEXT NOT NULL, `url` TEXT NOT NULL, `filename` TEXT NOT NULL, PRIMARY KEY(`real_id`))", + "fields": [ + { + "fieldPath": "realId", + "columnName": "real_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "messageId", + "columnName": "message_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "oneDriveId", + "columnName": "one_drive_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "filename", + "columnName": "filename", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "real_id" + ], + "autoGenerate": false + }, + "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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ReportingUnits", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `real_id` INTEGER NOT NULL, `short` TEXT NOT NULL, `sender_id` INTEGER NOT NULL, `sender_name` TEXT NOT NULL, `roles` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unitId", + "columnName": "real_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "shortName", + "columnName": "short", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "senderId", + "columnName": "sender_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "senderName", + "columnName": "sender_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "roles", + "columnName": "roles", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Recipients", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `real_id` TEXT NOT NULL, `name` TEXT NOT NULL, `real_name` TEXT NOT NULL, `login_id` INTEGER NOT NULL, `unit_id` INTEGER NOT NULL, `role` INTEGER NOT NULL, `hash` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "realId", + "columnName": "real_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "realName", + "columnName": "real_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "loginId", + "columnName": "login_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unitId", + "columnName": "unit_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "role", + "columnName": "role", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hash", + "columnName": "hash", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MobileDevices", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_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": "student_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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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)", + "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 + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "SchoolAnnouncements", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_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": "studentId", + "columnName": "student_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": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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, `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": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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, `type` TEXT NOT NULL, 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": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "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, '66946510bb620ae82686a5a1a31aba18')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a331c41f..5928c23a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -38,13 +38,14 @@ android:allowBackup="false" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" + android:networkSecurityConfig="@xml/network_security_config" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="false" android:theme="@style/WulkanowyTheme" - android:usesCleartextTraffic="true" tools:ignore="GoogleAppIndexingWarning,UnusedAttribute"> @@ -74,6 +75,7 @@ @@ -83,6 +85,7 @@ @@ -93,6 +96,23 @@ + + + + + + + + + + + @@ -119,11 +140,9 @@ - - - + android:exported="false" + tools:ignore="MissingClass" /> + - - - - - - + + diff --git a/app/src/main/java/io/github/wulkanowy/WulkanowyApp.kt b/app/src/main/java/io/github/wulkanowy/WulkanowyApp.kt index 4621c592..7cdeb622 100644 --- a/app/src/main/java/io/github/wulkanowy/WulkanowyApp.kt +++ b/app/src/main/java/io/github/wulkanowy/WulkanowyApp.kt @@ -1,12 +1,10 @@ package io.github.wulkanowy -import android.annotation.SuppressLint import android.app.Application import android.util.Log.DEBUG import android.util.Log.INFO import android.util.Log.VERBOSE import android.webkit.WebView -import androidx.fragment.app.FragmentManager import androidx.hilt.work.HiltWorkerFactory import androidx.work.Configuration import com.yariksoffice.lingver.Lingver @@ -41,10 +39,8 @@ class WulkanowyApp : Application(), Configuration.Provider { @Inject lateinit var analyticsHelper: AnalyticsHelper - @SuppressLint("UnsafeOptInUsageWarning") override fun onCreate() { super.onCreate() - FragmentManager.enableNewStateManager(false) initializeAppLanguage() themeManager.applyDefaultTheme() initLogging() diff --git a/app/src/main/java/io/github/wulkanowy/data/RepositoryModule.kt b/app/src/main/java/io/github/wulkanowy/data/DataModule.kt similarity index 69% rename from app/src/main/java/io/github/wulkanowy/data/RepositoryModule.kt rename to app/src/main/java/io/github/wulkanowy/data/DataModule.kt index a1c3cbbb..cac3ffc2 100644 --- a/app/src/main/java/io/github/wulkanowy/data/RepositoryModule.kt +++ b/app/src/main/java/io/github/wulkanowy/data/DataModule.kt @@ -2,62 +2,100 @@ package io.github.wulkanowy.data import android.content.Context import android.content.SharedPreferences -import android.content.res.AssetManager -import android.content.res.Resources import androidx.preference.PreferenceManager import com.chuckerteam.chucker.api.ChuckerCollector import com.chuckerteam.chucker.api.ChuckerInterceptor import com.chuckerteam.chucker.api.RetentionManager -import com.squareup.moshi.Moshi import com.fredporciuncula.flow.preferences.FlowSharedPreferences +import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent +import io.github.wulkanowy.data.api.AdminMessageService import io.github.wulkanowy.data.db.AppDatabase import io.github.wulkanowy.data.db.SharedPrefProvider import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.AppInfo import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.json.Json +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit +import retrofit2.create import timber.log.Timber +import java.util.concurrent.TimeUnit import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) -internal class RepositoryModule { +internal class DataModule { @Singleton @Provides - fun provideSdk(chuckerCollector: ChuckerCollector, @ApplicationContext context: Context): Sdk { - return Sdk().apply { + fun provideSdk(chuckerInterceptor: ChuckerInterceptor) = + Sdk().apply { androidVersion = android.os.Build.VERSION.RELEASE buildTag = android.os.Build.MODEL setSimpleHttpLogger { Timber.d(it) } // for debug only - addInterceptor( - ChuckerInterceptor.Builder(context) - .collector(chuckerCollector) - .alwaysReadResponseBody(true) - .build(), network = true - ) + addInterceptor(chuckerInterceptor, network = true) } - } @Singleton @Provides fun provideChuckerCollector( @ApplicationContext context: Context, prefRepository: PreferencesRepository - ): ChuckerCollector { - return ChuckerCollector( - context = context, - showNotification = prefRepository.isDebugNotificationEnable, - retentionPeriod = RetentionManager.Period.ONE_HOUR - ) - } + ) = ChuckerCollector( + context = context, + showNotification = prefRepository.isDebugNotificationEnable, + retentionPeriod = RetentionManager.Period.ONE_HOUR + ) + + @Singleton + @Provides + fun provideChuckerInterceptor( + @ApplicationContext context: Context, + chuckerCollector: ChuckerCollector + ) = ChuckerInterceptor.Builder(context) + .collector(chuckerCollector) + .alwaysReadResponseBody(true) + .build() + + @Singleton + @Provides + fun provideOkHttpClient(chuckerInterceptor: ChuckerInterceptor): OkHttpClient = + OkHttpClient.Builder() + .addNetworkInterceptor(chuckerInterceptor) + .addInterceptor(HttpLoggingInterceptor().apply { + level = HttpLoggingInterceptor.Level.BASIC + }) + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .build() + + @OptIn(ExperimentalSerializationApi::class) + @Singleton + @Provides + fun provideRetrofit( + okHttpClient: OkHttpClient, + json: Json, + appInfo: AppInfo + ): Retrofit = Retrofit.Builder() + .baseUrl(appInfo.messagesBaseUrl) + .client(okHttpClient) + .addConverterFactory(json.asConverterFactory("application/json".toMediaType())) + .build() + + @Singleton + @Provides + fun provideAdminMessageService(retrofit: Retrofit): AdminMessageService = retrofit.create() @Singleton @Provides @@ -67,14 +105,6 @@ internal class RepositoryModule { appInfo: AppInfo ) = AppDatabase.newInstance(context, sharedPrefProvider, appInfo) - @Singleton - @Provides - fun provideResources(@ApplicationContext context: Context): Resources = context.resources - - @Singleton - @Provides - fun provideAssets(@ApplicationContext context: Context): AssetManager = context.assets - @Singleton @Provides fun provideSharedPref(@ApplicationContext context: Context): SharedPreferences = @@ -88,7 +118,9 @@ internal class RepositoryModule { @Singleton @Provides - fun provideMoshi() = Moshi.Builder().build() + fun provideJson() = Json { + ignoreUnknownKeys = true + } @Singleton @Provides @@ -202,4 +234,12 @@ internal class RepositoryModule { @Singleton @Provides fun provideSchoolAnnouncementDao(database: AppDatabase) = database.schoolAnnouncementDao + + @Singleton + @Provides + fun provideNotificationDao(database: AppDatabase) = database.notificationDao + + @Singleton + @Provides + fun provideAdminMessageDao(database: AppDatabase) = database.adminMessagesDao } diff --git a/app/src/main/java/io/github/wulkanowy/data/api/AdminMessageService.kt b/app/src/main/java/io/github/wulkanowy/data/api/AdminMessageService.kt new file mode 100644 index 00000000..23f5af24 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/api/AdminMessageService.kt @@ -0,0 +1,12 @@ +package io.github.wulkanowy.data.api + +import io.github.wulkanowy.data.db.entities.AdminMessage +import retrofit2.http.GET +import javax.inject.Singleton + +@Singleton +interface AdminMessageService { + + @GET("/v1.json") + suspend fun getAdminMessages(): List +} \ No newline at end of file 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 0dca9aa1..7e6d609f 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 @@ -6,11 +6,11 @@ import androidx.room.Room import androidx.room.RoomDatabase import androidx.room.RoomDatabase.JournalMode.TRUNCATE 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.SchoolAnnouncementDao import io.github.wulkanowy.data.db.dao.ExamDao import io.github.wulkanowy.data.db.dao.GradeDao import io.github.wulkanowy.data.db.dao.GradePartialStatisticsDao @@ -23,8 +23,10 @@ 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.ReportingUnitDao +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 @@ -34,11 +36,11 @@ 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.SchoolAnnouncement import io.github.wulkanowy.data.db.entities.Exam import io.github.wulkanowy.data.db.entities.Grade import io.github.wulkanowy.data.db.entities.GradePartialStatistics @@ -51,9 +53,11 @@ 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.ReportingUnit 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 @@ -95,6 +99,10 @@ 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.Migration5 import io.github.wulkanowy.data.db.migrations.Migration6 import io.github.wulkanowy.data.db.migrations.Migration7 @@ -134,6 +142,8 @@ import javax.inject.Singleton StudentInfo::class, TimetableHeader::class, SchoolAnnouncement::class, + Notification::class, + AdminMessage::class ], version = AppDatabase.VERSION_SCHEMA, exportSchema = true @@ -142,7 +152,7 @@ import javax.inject.Singleton abstract class AppDatabase : RoomDatabase() { companion object { - const val VERSION_SCHEMA = 39 + const val VERSION_SCHEMA = 43 fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf( Migration2(), @@ -183,6 +193,10 @@ abstract class AppDatabase : RoomDatabase() { Migration37(), Migration38(), Migration39(), + Migration40(), + Migration41(sharedPrefProvider), + Migration42(), + Migration43() ) fun newInstance( @@ -252,4 +266,8 @@ abstract class AppDatabase : RoomDatabase() { abstract val timetableHeaderDao: TimetableHeaderDao abstract val schoolAnnouncementDao: SchoolAnnouncementDao + + abstract val notificationDao: NotificationDao + + abstract val adminMessagesDao: AdminMessageDao } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/Converters.kt b/app/src/main/java/io/github/wulkanowy/data/db/Converters.kt index def0b371..1993c433 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/Converters.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/Converters.kt @@ -1,9 +1,10 @@ package io.github.wulkanowy.data.db import androidx.room.TypeConverter -import com.squareup.moshi.Moshi -import com.squareup.moshi.Types -import io.github.wulkanowy.data.db.adapters.PairAdapterFactory +import kotlinx.serialization.SerializationException +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json import java.time.Instant import java.time.LocalDate import java.time.LocalDateTime @@ -13,15 +14,7 @@ import java.util.Date class Converters { - private val moshi by lazy { Moshi.Builder().add(PairAdapterFactory).build() } - - private val integerListAdapter by lazy { - moshi.adapter>(Types.newParameterizedType(List::class.java, Integer::class.java)) - } - - private val stringListPairAdapter by lazy { - moshi.adapter>>(Types.newParameterizedType(List::class.java, Pair::class.java, String::class.java, String::class.java)) - } + private val json = Json @TypeConverter fun timestampToDate(value: Long?): LocalDate? = value?.run { @@ -51,21 +44,25 @@ class Converters { @TypeConverter fun intListToJson(list: List): String { - return integerListAdapter.toJson(list) + return json.encodeToString(list) } @TypeConverter fun jsonToIntList(value: String): List { - return integerListAdapter.fromJson(value).orEmpty() + return json.decodeFromString(value) } @TypeConverter fun stringPairListToJson(list: List>): String { - return stringListPairAdapter.toJson(list) + return json.encodeToString(list) } @TypeConverter fun jsonToStringPairList(value: String): List> { - return stringListPairAdapter.fromJson(value).orEmpty() + return try { + json.decodeFromString(value) + } catch (e: SerializationException) { + emptyList() // handle errors from old gson Pair serialized data + } } } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/SharedPrefProvider.kt b/app/src/main/java/io/github/wulkanowy/data/db/SharedPrefProvider.kt index 0623d403..4929f046 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/SharedPrefProvider.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/SharedPrefProvider.kt @@ -22,11 +22,14 @@ class SharedPrefProvider @Inject constructor( fun getString(key: String) = sharedPref.getString(key, null) - fun getString(key: String, defaultValue: String): String = sharedPref.getString(key, defaultValue) ?: defaultValue + fun getString(key: String, defaultValue: String): String = + sharedPref.getString(key, defaultValue) ?: defaultValue - fun getBoolean(key: String, defaultValue: Boolean): Boolean = sharedPref.getBoolean(key, defaultValue) + fun getBoolean(key: String, defaultValue: Boolean): Boolean = + sharedPref.getBoolean(key, defaultValue) - fun putBoolean(key: String, value: Boolean, sync: Boolean = false) = sharedPref.edit(sync) { putBoolean(key, value) } + fun putBoolean(key: String, value: Boolean, sync: Boolean = false) = + sharedPref.edit(sync) { putBoolean(key, value) } fun putString(key: String, value: String?, sync: Boolean = false) { sharedPref.edit(sync) { putString(key, value) } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/adapters/PairAdapterFactory.kt b/app/src/main/java/io/github/wulkanowy/data/db/adapters/PairAdapterFactory.kt deleted file mode 100644 index 4a9b168d..00000000 --- a/app/src/main/java/io/github/wulkanowy/data/db/adapters/PairAdapterFactory.kt +++ /dev/null @@ -1,68 +0,0 @@ -package io.github.wulkanowy.data.db.adapters - -import com.squareup.moshi.JsonAdapter -import com.squareup.moshi.JsonReader -import com.squareup.moshi.JsonWriter -import com.squareup.moshi.Moshi -import com.squareup.moshi.Types -import java.lang.reflect.ParameterizedType -import java.lang.reflect.Type - -object PairAdapterFactory : JsonAdapter.Factory { - - override fun create(type: Type, annotations: MutableSet, moshi: Moshi): JsonAdapter<*>? { - if (type !is ParameterizedType || List::class.java != type.rawType) return null - if (type.actualTypeArguments[0] != Pair::class.java) return null - - val listType = Types.newParameterizedType(List::class.java, Map::class.java, String::class.java) - val listAdapter = moshi.adapter>>(listType) - - val mapType = Types.newParameterizedType(MutableMap::class.java, String::class.java, String::class.java) - val mapAdapter = moshi.adapter>(mapType) - - return PairAdapter(listAdapter, mapAdapter) - } - - private class PairAdapter( - private val listAdapter: JsonAdapter>>, - private val mapAdapter: JsonAdapter>, - ) : JsonAdapter>>() { - - override fun toJson(writer: JsonWriter, value: List>?) { - writer.beginArray() - value?.forEach { - writer.beginObject() - writer.name("first").value(it.first) - writer.name("second").value(it.second) - writer.endObject() - } - writer.endArray() - } - - override fun fromJson(reader: JsonReader): List>? { - return if (reader.peek() == JsonReader.Token.BEGIN_OBJECT) deserializeMoshiMap(reader) - else deserializeGsonPair(reader) - } - - // for compatibility with 0.21.0 - private fun deserializeMoshiMap(reader: JsonReader): List>? { - val map = mapAdapter.fromJson(reader) ?: return null - - return map.entries.map { - it.key to it.value - } - } - - private fun deserializeGsonPair(reader: JsonReader): List>? { - val list = listAdapter.fromJson(reader) ?: return null - - return list.map { - require(it.size == 2) { - "pair with more or less than two elements: $list" - } - - it["first"].orEmpty() to it["second"].orEmpty() - } - } - } -} diff --git a/app/src/main/java/io/github/wulkanowy/data/db/dao/AdminMessageDao.kt b/app/src/main/java/io/github/wulkanowy/data/db/dao/AdminMessageDao.kt new file mode 100644 index 00000000..87f4812d --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/db/dao/AdminMessageDao.kt @@ -0,0 +1,25 @@ +package io.github.wulkanowy.data.db.dao + +import androidx.room.Dao +import androidx.room.Query +import androidx.room.Transaction +import io.github.wulkanowy.data.db.entities.AdminMessage +import kotlinx.coroutines.flow.Flow +import javax.inject.Singleton + +@Singleton +@Dao +abstract class AdminMessageDao : BaseDao { + + @Query("SELECT * FROM AdminMessages") + abstract fun loadAll(): Flow> + + @Transaction + open suspend fun removeOldAndSaveNew( + oldMessages: List, + newMessages: List + ) { + deleteAll(oldMessages) + insertAll(newMessages) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/data/db/dao/AttendanceDao.kt b/app/src/main/java/io/github/wulkanowy/data/db/dao/AttendanceDao.kt index 8ef3fd44..c6c255a1 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/dao/AttendanceDao.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/dao/AttendanceDao.kt @@ -11,6 +11,11 @@ import javax.inject.Singleton @Dao interface AttendanceDao : BaseDao { - @Query("SELECT * FROM Attendance WHERE diary_id = :diaryId AND student_id = :studentId AND date >= :from AND date <= :end") - fun loadAll(diaryId: Int, studentId: Int, from: LocalDate, end: LocalDate): Flow> + @Query("SELECT * FROM Attendance WHERE diary_id = :diaryId AND student_id = :studentId AND date >= :start AND date <= :end") + fun loadAll( + diaryId: Int, + studentId: Int, + start: LocalDate, + end: LocalDate + ): Flow> } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/dao/NotificationDao.kt b/app/src/main/java/io/github/wulkanowy/data/db/dao/NotificationDao.kt new file mode 100644 index 00000000..c5ae21bc --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/db/dao/NotificationDao.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.Notification +import kotlinx.coroutines.flow.Flow +import javax.inject.Singleton + +@Singleton +@Dao +interface NotificationDao : BaseDao { + + @Query("SELECT * FROM Notifications WHERE student_id = :studentId OR student_id = -1") + fun loadAll(studentId: Long): Flow> +} \ No newline at end of file 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 0ad2ee59..3dda8a44 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 @@ -14,33 +14,39 @@ import javax.inject.Singleton @Singleton @Dao -interface StudentDao { +abstract class StudentDao { @Insert(onConflict = ABORT) - suspend fun insertAll(student: List): List + abstract suspend fun insertAll(student: List): List @Delete - suspend fun delete(student: Student) + abstract suspend fun delete(student: Student) @Update(entity = Student::class) - suspend fun update(studentNickAndAvatar: StudentNickAndAvatar) + abstract suspend fun update(studentNickAndAvatar: StudentNickAndAvatar) @Query("SELECT * FROM Students WHERE is_current = 1") - suspend fun loadCurrent(): Student? + abstract suspend fun loadCurrent(): Student? @Query("SELECT * FROM Students WHERE id = :id") - suspend fun loadById(id: Long): Student? + abstract suspend fun loadById(id: Long): Student? @Query("SELECT * FROM Students") - suspend fun loadAll(): List + abstract suspend fun loadAll(): List @Transaction @Query("SELECT * FROM Students") - suspend fun loadStudentsWithSemesters(): List + abstract suspend fun loadStudentsWithSemesters(): List @Query("UPDATE Students SET is_current = 1 WHERE id = :id") - suspend fun updateCurrent(id: Long) + abstract suspend fun updateCurrent(id: Long) @Query("UPDATE Students SET is_current = 0") - suspend fun resetCurrent() + abstract suspend fun resetCurrent() + + @Transaction + open suspend fun switchCurrent(id: Long) { + resetCurrent() + updateCurrent(id) + } } 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 new file mode 100644 index 00000000..b7b0cf58 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/AdminMessage.kt @@ -0,0 +1,37 @@ +package io.github.wulkanowy.data.db.entities + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import kotlinx.serialization.Serializable + +@Serializable +@Entity(tableName = "AdminMessages") +data class AdminMessage( + + @PrimaryKey + val id: Int, + + val title: String, + + val content: String, + + @ColumnInfo(name = "version_name") + val versionMin: Int? = null, + + @ColumnInfo(name = "version_max") + val versionMax: Int? = null, + + @ColumnInfo(name = "target_register_host") + val targetRegisterHost: String? = null, + + @ColumnInfo(name = "target_flavor") + val targetFlavor: String? = null, + + @ColumnInfo(name = "destination_url") + val destinationUrl: String? = null, + + val priority: String, + + val type: String +) diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/Attendance.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/Attendance.kt index f141d5d5..b40dd52e 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/entities/Attendance.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/Attendance.kt @@ -47,4 +47,7 @@ data class Attendance( @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/entities/Homework.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/Homework.kt index 04ee1e8c..4538cf31 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/entities/Homework.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/Homework.kt @@ -40,4 +40,7 @@ data class Homework( @ColumnInfo(name = "is_notified") var isNotified: Boolean = true + + @ColumnInfo(name = "is_added_by_user") + var isAddedByUser: Boolean = false } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/Notification.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/Notification.kt new file mode 100644 index 00000000..740137f0 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/Notification.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 io.github.wulkanowy.services.sync.notifications.NotificationType +import java.time.LocalDateTime + +@Entity(tableName = "Notifications") +data class Notification( + + @ColumnInfo(name = "student_id") + val studentId: Long, + + val title: String, + + val content: String, + + val type: NotificationType, + + val date: LocalDateTime, + + val data: String? = null +) { + @PrimaryKey(autoGenerate = true) + var id: Long = 0 +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/Recipient.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/Recipient.kt index 60e67d32..22332270 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/entities/Recipient.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/Recipient.kt @@ -3,10 +3,9 @@ package io.github.wulkanowy.data.db.entities import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey -import com.squareup.moshi.JsonClass import java.io.Serializable -@JsonClass(generateAdapter = true) +@kotlinx.serialization.Serializable @Entity(tableName = "Recipients") data class Recipient( diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/Timetable.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/Timetable.kt index 1bf159ef..29b3737b 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/entities/Timetable.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/Timetable.kt @@ -50,4 +50,7 @@ data class Timetable( @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/Migration40.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration40.kt new file mode 100644 index 00000000..6d2795c7 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration40.kt @@ -0,0 +1,23 @@ +package io.github.wulkanowy.data.db.migrations + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +class Migration40 : Migration(39, 40) { + + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL( + """ + CREATE TABLE IF NOT EXISTS `Notifications` ( + `student_id` INTEGER NOT NULL, + `title` TEXT NOT NULL, + `content` TEXT NOT NULL, + `type` TEXT NOT NULL, + `date` INTEGER NOT NULL, + `data` TEXT, + `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL + ) + """ + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration41.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration41.kt new file mode 100644 index 00000000..0080e057 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration41.kt @@ -0,0 +1,21 @@ +package io.github.wulkanowy.data.db.migrations + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase +import io.github.wulkanowy.data.db.SharedPrefProvider +import io.github.wulkanowy.ui.modules.grade.GradeExpandMode + +class Migration41(private val sharedPrefProvider: SharedPrefProvider) : Migration(40, 41) { + + override fun migrate(database: SupportSQLiteDatabase) { + migrateSharedPreferences() + database.execSQL("ALTER TABLE Homework ADD COLUMN is_added_by_user INTEGER NOT NULL DEFAULT 0") + } + + private fun migrateSharedPreferences() { + if (sharedPrefProvider.getBoolean("pref_key_expand_grade", false)) { + sharedPrefProvider.putString("pref_key_expand_grade_mode", GradeExpandMode.ALWAYS_EXPANDED.value) + } + sharedPrefProvider.delete("pref_key_expand_grade") + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration42.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration42.kt new file mode 100644 index 00000000..3d66f301 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration42.kt @@ -0,0 +1,24 @@ +package io.github.wulkanowy.data.db.migrations + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +class Migration42 : Migration(41, 42) { + + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL( + """CREATE TABLE IF NOT EXISTS `AdminMessages` ( + `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, + `type` TEXT NOT NULL, + PRIMARY KEY(`id`))""" + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration43.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration43.kt new file mode 100644 index 00000000..68c2834d --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration43.kt @@ -0,0 +1,12 @@ +package io.github.wulkanowy.data.db.migrations + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +class Migration43 : Migration(42, 43) { + + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("ALTER TABLE Timetable ADD COLUMN is_notified INTEGER NOT NULL DEFAULT 1") + database.execSQL("ALTER TABLE Attendance ADD COLUMN is_notified INTEGER NOT NULL DEFAULT 1") + } +} diff --git a/app/src/main/java/io/github/wulkanowy/data/pojos/Contributor.kt b/app/src/main/java/io/github/wulkanowy/data/pojos/Contributor.kt index d2338c28..4165b3f1 100644 --- a/app/src/main/java/io/github/wulkanowy/data/pojos/Contributor.kt +++ b/app/src/main/java/io/github/wulkanowy/data/pojos/Contributor.kt @@ -1,8 +1,8 @@ package io.github.wulkanowy.data.pojos -import com.squareup.moshi.JsonClass +import kotlinx.serialization.Serializable -@JsonClass(generateAdapter = true) +@Serializable class Contributor( val displayName: String, val githubUsername: String diff --git a/app/src/main/java/io/github/wulkanowy/data/pojos/MessageDraft.kt b/app/src/main/java/io/github/wulkanowy/data/pojos/MessageDraft.kt index a79b70cd..2e568e37 100644 --- a/app/src/main/java/io/github/wulkanowy/data/pojos/MessageDraft.kt +++ b/app/src/main/java/io/github/wulkanowy/data/pojos/MessageDraft.kt @@ -1,9 +1,9 @@ package io.github.wulkanowy.data.pojos -import com.squareup.moshi.JsonClass import io.github.wulkanowy.ui.modules.message.send.RecipientChipItem +import kotlinx.serialization.Serializable -@JsonClass(generateAdapter = true) +@Serializable data class MessageDraft( val recipients: List, val subject: String, diff --git a/app/src/main/java/io/github/wulkanowy/data/pojos/Notification.kt b/app/src/main/java/io/github/wulkanowy/data/pojos/Notification.kt deleted file mode 100644 index ca274937..00000000 --- a/app/src/main/java/io/github/wulkanowy/data/pojos/Notification.kt +++ /dev/null @@ -1,36 +0,0 @@ -package io.github.wulkanowy.data.pojos - -import androidx.annotation.DrawableRes -import androidx.annotation.PluralsRes -import androidx.annotation.StringRes -import io.github.wulkanowy.services.sync.notifications.NotificationType -import io.github.wulkanowy.ui.modules.main.MainView - -sealed interface Notification { - val type: NotificationType - val startMenu: MainView.Section - val icon: Int - val titleStringRes: Int - val contentStringRes: Int -} - -data class MultipleNotifications( - override val type: NotificationType, - override val startMenu: MainView.Section, - @DrawableRes override val icon: Int, - @PluralsRes override val titleStringRes: Int, - @PluralsRes override val contentStringRes: Int, - - @PluralsRes val summaryStringRes: Int, - val lines: List, -) : Notification - -data class OneNotification( - override val type: NotificationType, - override val startMenu: MainView.Section, - @DrawableRes override val icon: Int, - @StringRes override val titleStringRes: Int, - @StringRes override val contentStringRes: Int, - - val contentValues: List, -) : Notification diff --git a/app/src/main/java/io/github/wulkanowy/data/pojos/NotificationData.kt b/app/src/main/java/io/github/wulkanowy/data/pojos/NotificationData.kt new file mode 100644 index 00000000..0748ba64 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/pojos/NotificationData.kt @@ -0,0 +1,19 @@ +package io.github.wulkanowy.data.pojos + +import android.content.Intent +import io.github.wulkanowy.services.sync.notifications.NotificationType + +data class NotificationData( + val intentToStart: Intent, + val title: String, + val content: String +) + +data class GroupNotificationData( + val notificationDataList: List, + val title: String, + val content: String, + val intentToStart: Intent, + val type: NotificationType +) + diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/AdminMessageRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/AdminMessageRepository.kt new file mode 100644 index 00000000..1b17e3bf --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/AdminMessageRepository.kt @@ -0,0 +1,52 @@ +package io.github.wulkanowy.data.repositories + +import io.github.wulkanowy.data.api.AdminMessageService +import io.github.wulkanowy.data.db.dao.AdminMessageDao +import io.github.wulkanowy.data.db.entities.Student +import io.github.wulkanowy.utils.AppInfo +import io.github.wulkanowy.utils.AutoRefreshHelper +import io.github.wulkanowy.utils.networkBoundResource +import kotlinx.coroutines.sync.Mutex +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class AdminMessageRepository @Inject constructor( + private val adminMessageService: AdminMessageService, + private val adminMessageDao: AdminMessageDao, + private val appInfo: AppInfo, + private val refreshHelper: AutoRefreshHelper, +) { + private val saveFetchResultMutex = Mutex() + + private val cacheKey = "admin_messages" + + suspend fun getAdminMessages(student: Student, forceRefresh: Boolean) = networkBoundResource( + mutex = saveFetchResultMutex, + query = { adminMessageDao.loadAll() }, + fetch = { adminMessageService.getAdminMessages() }, + shouldFetch = { + refreshHelper.shouldBeRefreshed(cacheKey) || forceRefresh + }, + saveFetchResult = { oldItems, newItems -> + adminMessageDao.removeOldAndSaveNew(oldItems, newItems) + refreshHelper.updateLastRefreshTimestamp(cacheKey) + }, + showSavedOnLoading = false, + mapResult = { adminMessages -> + adminMessages.filter { adminMessage -> + val isCorrectRegister = adminMessage.targetRegisterHost?.let { + student.scrapperBaseUrl.contains(it, true) + } ?: true + val isCorrectFlavor = + adminMessage.targetFlavor?.equals(appInfo.buildFlavor, true) ?: true + val isCorrectMaxVersion = + adminMessage.versionMax?.let { it >= appInfo.versionCode } ?: true + val isCorrectMinVersion = + adminMessage.versionMin?.let { it <= appInfo.versionCode } ?: true + + isCorrectRegister && isCorrectFlavor && isCorrectMaxVersion && isCorrectMinVersion + }.maxByOrNull { it.id } + } + ) +} diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/AppCreatorRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/AppCreatorRepository.kt index 71b7ea94..cbaa12bd 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/AppCreatorRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/AppCreatorRepository.kt @@ -1,25 +1,27 @@ package io.github.wulkanowy.data.repositories -import android.content.res.AssetManager -import com.squareup.moshi.Moshi -import com.squareup.moshi.Types +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.data.pojos.Contributor import io.github.wulkanowy.utils.DispatchersProvider import kotlinx.coroutines.withContext +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.decodeFromStream import javax.inject.Inject import javax.inject.Singleton @Singleton class AppCreatorRepository @Inject constructor( - private val assets: AssetManager, - private val dispatchers: DispatchersProvider + @ApplicationContext private val context: Context, + private val dispatchers: DispatchersProvider, + private val json: Json, ) { + @OptIn(ExperimentalSerializationApi::class) @Suppress("BlockingMethodInNonBlockingContext") - suspend fun getAppCreators() = withContext(dispatchers.backgroundThread) { - val moshi = Moshi.Builder().build() - val type = Types.newParameterizedType(List::class.java, Contributor::class.java) - val adapter = moshi.adapter>(type) - adapter.fromJson(assets.open("contributors.json").bufferedReader().use { it.readText() }) + suspend fun getAppCreators() = withContext(dispatchers.io) { + val inputStream = context.assets.open("contributors.json").buffered() + json.decodeFromStream>(inputStream) } } 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 ffccb059..ec919817 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 @@ -14,6 +14,7 @@ import io.github.wulkanowy.utils.monday import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.sunday import io.github.wulkanowy.utils.uniqueSubtract +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.sync.Mutex import java.time.LocalDate import java.time.LocalDateTime @@ -32,10 +33,24 @@ class AttendanceRepository @Inject constructor( private val cacheKey = "attendance" - fun getAttendance(student: Student, semester: Semester, start: LocalDate, end: LocalDate, forceRefresh: Boolean) = networkBoundResource( + fun getAttendance( + student: Student, + semester: Semester, + start: LocalDate, + end: LocalDate, + forceRefresh: Boolean, + notify: Boolean = false, + ) = networkBoundResource( mutex = saveFetchResultMutex, - shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester, start, end)) }, - query = { attendanceDb.loadAll(semester.diaryId, semester.studentId, start.monday, end.sunday) }, + shouldFetch = { + val isExpired = refreshHelper.shouldBeRefreshed( + key = getRefreshKey(cacheKey, semester, start, end) + ) + it.isEmpty() || forceRefresh || isExpired + }, + query = { + attendanceDb.loadAll(semester.diaryId, semester.studentId, start.monday, end.sunday) + }, fetch = { sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) .getAttendance(start.monday, end.sunday, semester.semesterId) @@ -43,19 +58,39 @@ class AttendanceRepository @Inject constructor( }, saveFetchResult = { old, new -> attendanceDb.deleteAll(old uniqueSubtract new) - attendanceDb.insertAll(new uniqueSubtract old) + val attendanceToAdd = (new uniqueSubtract old).map { newAttendance -> + newAttendance.apply { if (notify) isNotified = false } + } + attendanceDb.insertAll(attendanceToAdd) refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester, start, end)) }, filterResult = { it.filter { item -> item.date in start..end } } ) - suspend fun excuseForAbsence(student: Student, semester: Semester, absenceList: List, reason: String? = null) { - sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear).excuseForAbsence(absenceList.map { attendance -> + fun getAttendanceFromDatabase( + semester: Semester, + start: LocalDate, + end: LocalDate + ): Flow> { + return attendanceDb.loadAll(semester.diaryId, semester.studentId, start, end) + } + + suspend fun updateTimetable(timetable: List) { + return attendanceDb.updateAll(timetable) + } + + suspend fun excuseForAbsence( + student: Student, semester: Semester, + absenceList: List, reason: String? = null + ) { + val items = absenceList.map { attendance -> Absent( date = LocalDateTime.of(attendance.date, LocalTime.of(0, 0)), timeId = attendance.timeId ) - }, reason) + } + sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) + .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 58659914..bc1fb234 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 @@ -29,12 +29,12 @@ class AttendanceSummaryRepository @Inject constructor( student: Student, semester: Semester, subjectId: Int, - forceRefresh: Boolean + forceRefresh: Boolean, ) = networkBoundResource( mutex = saveFetchResultMutex, shouldFetch = { - it.isEmpty() || forceRefresh - || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester)) + val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, semester)) + it.isEmpty() || forceRefresh || isExpired }, query = { attendanceDb.loadAll(semester.diaryId, semester.studentId, subjectId) }, fetch = { 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 99ef56f4..c2e5a721 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 @@ -28,10 +28,28 @@ class CompletedLessonsRepository @Inject constructor( private val cacheKey = "completed" - fun getCompletedLessons(student: Student, semester: Semester, start: LocalDate, end: LocalDate, forceRefresh: Boolean) = networkBoundResource( + fun getCompletedLessons( + student: Student, + semester: Semester, + start: LocalDate, + end: LocalDate, + forceRefresh: Boolean, + ) = networkBoundResource( mutex = saveFetchResultMutex, - shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester, start, end)) }, - query = { completedLessonsDb.loadAll(semester.studentId, semester.diaryId, start.monday, end.sunday) }, + shouldFetch = { + val isExpired = refreshHelper.shouldBeRefreshed( + key = getRefreshKey(cacheKey, semester, start, end) + ) + it.isEmpty() || forceRefresh || isExpired + }, + query = { + completedLessonsDb.loadAll( + studentId = semester.studentId, + diaryId = semester.diaryId, + from = start.monday, + end = end.sunday + ) + }, fetch = { sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) .getCompletedLessons(start.monday, end.sunday) 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 16d7c3c6..e3227183 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 @@ -35,12 +35,12 @@ class ConferenceRepository @Inject constructor( semester: Semester, forceRefresh: Boolean, notify: Boolean = false, - startDate: LocalDateTime = LocalDateTime.ofInstant(Instant.EPOCH, ZoneOffset.UTC) + startDate: LocalDateTime = LocalDateTime.ofInstant(Instant.EPOCH, ZoneOffset.UTC), ) = networkBoundResource( mutex = saveFetchResultMutex, shouldFetch = { - it.isEmpty() || forceRefresh - || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester)) + val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, semester)) + it.isEmpty() || forceRefresh || isExpired }, query = { conferenceDb.loadAll(semester.diaryId, student.studentId, 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 93d5a47c..9bdac065 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 @@ -36,14 +36,14 @@ class ExamRepository @Inject constructor( start: LocalDate, end: LocalDate, forceRefresh: Boolean, - notify: Boolean = false + notify: Boolean = false, ) = networkBoundResource( mutex = saveFetchResultMutex, shouldFetch = { - val isShouldBeRefreshed = refreshHelper.isShouldBeRefreshed( + val isExpired = refreshHelper.shouldBeRefreshed( key = getRefreshKey(cacheKey, semester, start, end) ) - it.isEmpty() || forceRefresh || isShouldBeRefreshed + it.isEmpty() || forceRefresh || isExpired }, query = { examDb.loadAll( 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 d8417f8a..6c574b48 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 @@ -37,13 +37,12 @@ class GradeRepository @Inject constructor( student: Student, semester: Semester, forceRefresh: Boolean, - notify: Boolean = false + notify: Boolean = false, ) = networkBoundResource( mutex = saveFetchResultMutex, shouldFetch = { (details, summaries) -> - val isShouldBeRefreshed = - refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester)) - details.isEmpty() || summaries.isEmpty() || forceRefresh || isShouldBeRefreshed + val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, semester)) + details.isEmpty() || summaries.isEmpty() || forceRefresh || isExpired }, query = { val detailsFlow = gradeDb.loadAll(semester.semesterId, semester.studentId) @@ -71,8 +70,8 @@ class GradeRepository @Inject constructor( newDetails: List, notify: Boolean ) { - val notifyBreakDate = - oldGrades.maxByOrNull { it.date }?.date ?: student.registrationDate.toLocalDate() + val notifyBreakDate = oldGrades.maxByOrNull {it.date } + ?.date ?: student.registrationDate.toLocalDate() gradeDb.deleteAll(oldGrades uniqueSubtract newDetails) gradeDb.insertAll((newDetails uniqueSubtract oldGrades).onEach { if (it.date >= notifyBreakDate) it.apply { @@ -89,8 +88,7 @@ class GradeRepository @Inject constructor( ) { gradeSummaryDb.deleteAll(oldSummaries uniqueSubtract newSummary) gradeSummaryDb.insertAll((newSummary uniqueSubtract oldSummaries).onEach { summary -> - val oldSummary = - oldSummaries.find { oldSummary -> oldSummary.subject == summary.subject } + val oldSummary = oldSummaries.find { old -> old.subject == summary.subject } summary.isPredictedGradeNotified = when { summary.predictedGrade.isEmpty() -> true notify && oldSummary?.predictedGrade != summary.predictedGrade -> false 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 9cd8e711..6c36f163 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 @@ -39,9 +39,19 @@ class GradeStatisticsRepository @Inject constructor( private val semesterCacheKey = "grade_stats_semester" private val pointsCacheKey = "grade_stats_points" - fun getGradesPartialStatistics(student: Student, semester: Semester, subjectName: String, forceRefresh: Boolean) = networkBoundResource( + fun getGradesPartialStatistics( + student: Student, + semester: Semester, + subjectName: String, + forceRefresh: Boolean, + ) = networkBoundResource( mutex = partialMutex, - shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(partialCacheKey, semester)) }, + shouldFetch = { + val isExpired = refreshHelper.shouldBeRefreshed( + key = getRefreshKey(partialCacheKey, semester) + ) + it.isEmpty() || forceRefresh || isExpired + }, query = { gradePartialStatisticsDb.loadAll(semester.semesterId, semester.studentId) }, fetch = { sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) @@ -76,9 +86,19 @@ class GradeStatisticsRepository @Inject constructor( } ) - fun getGradesSemesterStatistics(student: Student, semester: Semester, subjectName: String, forceRefresh: Boolean) = networkBoundResource( + fun getGradesSemesterStatistics( + student: Student, + semester: Semester, + subjectName: String, + forceRefresh: Boolean, + ) = networkBoundResource( mutex = semesterMutex, - shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(semesterCacheKey, semester)) }, + shouldFetch = { + val isExpired = refreshHelper.shouldBeRefreshed( + key = getRefreshKey(semesterCacheKey, semester) + ) + it.isEmpty() || forceRefresh || isExpired + }, query = { gradeSemesterStatisticsDb.loadAll(semester.semesterId, semester.studentId) }, fetch = { sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) @@ -94,10 +114,12 @@ class GradeStatisticsRepository @Inject constructor( val itemsWithAverage = items.map { item -> item.copy().apply { val denominator = item.amounts.sum() - average = if (denominator == 0) "" else (item.amounts.mapIndexed { gradeValue, amount -> - (gradeValue + 1) * amount - }.sum().toDouble() / denominator).let { - "%.2f".format(Locale.FRANCE, it) + average = if (denominator == 0) "" else { + (item.amounts.mapIndexed { gradeValue, amount -> + (gradeValue + 1) * amount + }.sum().toDouble() / denominator).let { + "%.2f".format(Locale.FRANCE, it) + } } } } @@ -109,7 +131,9 @@ class GradeStatisticsRepository @Inject constructor( amounts = itemsWithAverage.map { it.amounts }.sumGradeAmounts(), studentGrade = 0 ).apply { - average = itemsWithAverage.mapNotNull { it.average.replace(",", ".").toDoubleOrNull() }.average().let { + average = itemsWithAverage.mapNotNull { + it.average.replace(",", ".").toDoubleOrNull() + }.average().let { "%.2f".format(Locale.FRANCE, it) } }).reversed() @@ -118,9 +142,17 @@ class GradeStatisticsRepository @Inject constructor( } ) - fun getGradesPointsStatistics(student: Student, semester: Semester, subjectName: String, forceRefresh: Boolean) = networkBoundResource( + fun getGradesPointsStatistics( + student: Student, + semester: Semester, + subjectName: String, + forceRefresh: Boolean, + ) = networkBoundResource( mutex = pointsMutex, - shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(pointsCacheKey, semester)) }, + shouldFetch = { + val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(pointsCacheKey, semester)) + it.isEmpty() || forceRefresh || isExpired + }, query = { gradePointsStatisticsDb.loadAll(semester.semesterId, semester.studentId) }, fetch = { sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) 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 23dd74c2..95a375a4 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 @@ -30,16 +30,19 @@ class HomeworkRepository @Inject constructor( private val cacheKey = "homework" fun getHomework( - student: Student, semester: Semester, - start: LocalDate, end: LocalDate, - forceRefresh: Boolean, notify: Boolean = false + student: Student, + semester: Semester, + start: LocalDate, + end: LocalDate, + forceRefresh: Boolean, + notify: Boolean = false, ) = networkBoundResource( mutex = saveFetchResultMutex, shouldFetch = { - val isShouldBeRefreshed = refreshHelper.isShouldBeRefreshed( + val isExpired = refreshHelper.shouldBeRefreshed( key = getRefreshKey(cacheKey, semester, start, end) ) - it.isEmpty() || forceRefresh || isShouldBeRefreshed + it.isEmpty() || forceRefresh || isExpired }, query = { homeworkDb.loadAll( @@ -58,8 +61,9 @@ class HomeworkRepository @Inject constructor( val homeWorkToSave = (new uniqueSubtract old).onEach { if (notify) it.isNotified = false } + val filteredOld = old.filterNot { it.isAddedByUser } - homeworkDb.deleteAll(old uniqueSubtract new) + homeworkDb.deleteAll(filteredOld uniqueSubtract new) homeworkDb.insertAll(homeWorkToSave) refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester, start, end)) @@ -76,4 +80,8 @@ class HomeworkRepository @Inject constructor( homeworkDb.loadAll(semester.semesterId, semester.studentId, start.monday, end.sunday) suspend fun updateHomework(homework: List) = homeworkDb.updateAll(homework) + + suspend fun saveHomework(homework: Homework) = homeworkDb.insertAll(listOf(homework)) + + suspend fun deleteHomework(homework: Homework) = homeworkDb.deleteAll(listOf(homework)) } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/LoggerRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/LoggerRepository.kt index 6d509b02..1a8cd6ea 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/LoggerRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/LoggerRepository.kt @@ -15,24 +15,23 @@ class LoggerRepository @Inject constructor( suspend fun getLastLogLines() = getLastModified().readText().split("\n") - suspend fun getLogFiles() = withContext(dispatchers.backgroundThread) { - File(context.filesDir.absolutePath).listFiles(File::isFile)?.filter { - it.name.endsWith(".log") - }!! + suspend fun getLogFiles() = withContext(dispatchers.io) { + File(context.filesDir.absolutePath).listFiles(File::isFile) + ?.filter { it.name.endsWith(".log") }!! } - private suspend fun getLastModified(): File { - return withContext(dispatchers.backgroundThread) { - var lastModifiedTime = Long.MIN_VALUE - var chosenFile: File? = null - File(context.filesDir.absolutePath).listFiles(File::isFile)?.forEach { file -> + private suspend fun getLastModified() = withContext(dispatchers.io) { + var lastModifiedTime = Long.MIN_VALUE + var chosenFile: File? = null + + File(context.filesDir.absolutePath).listFiles(File::isFile) + ?.forEach { file -> if (file.lastModified() > lastModifiedTime) { lastModifiedTime = file.lastModified() chosenFile = file } } - if (chosenFile == null) throw FileNotFoundException("Log file not found") - chosenFile!! - } + + chosenFile ?: throw FileNotFoundException("Log file not found") } } 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 b904b7db..41e824e5 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 @@ -23,11 +23,17 @@ class LuckyNumberRepository @Inject constructor( private val saveFetchResultMutex = Mutex() - fun getLuckyNumber(student: Student, forceRefresh: Boolean, notify: Boolean = false) = networkBoundResource( + fun getLuckyNumber( + student: Student, + forceRefresh: Boolean, + notify: Boolean = false, + ) = networkBoundResource( mutex = saveFetchResultMutex, shouldFetch = { it == null || forceRefresh }, query = { luckyNumberDb.load(student.studentId, now()) }, - fetch = { sdk.init(student).getLuckyNumber(student.schoolShortName)?.mapToEntity(student) }, + fetch = { + sdk.init(student).getLuckyNumber(student.schoolShortName)?.mapToEntity(student) + }, saveFetchResult = { old, new -> if (new != old) { old?.let { luckyNumberDb.deleteAll(listOfNotNull(it)) } @@ -41,9 +47,11 @@ class LuckyNumberRepository @Inject constructor( fun getLuckyNumberHistory(student: Student, start: LocalDate, end: LocalDate) = luckyNumberDb.getAll(student.studentId, start, end) - suspend fun getNotNotifiedLuckyNumber(student: Student) = luckyNumberDb.load(student.studentId, now()).map { - if (it?.isNotified == false) it else null - }.first() + suspend fun getNotNotifiedLuckyNumber(student: Student) = + luckyNumberDb.load(student.studentId, now()).map { + if (it?.isNotified == false) it else null + }.first() - suspend fun updateLuckyNumber(luckyNumber: LuckyNumber?) = luckyNumberDb.updateAll(listOfNotNull(luckyNumber)) + suspend fun updateLuckyNumber(luckyNumber: LuckyNumber?) = + luckyNumberDb.updateAll(listOfNotNull(luckyNumber)) } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt index 9977e1d5..224c69bd 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt @@ -1,7 +1,6 @@ package io.github.wulkanowy.data.repositories import android.content.Context -import com.squareup.moshi.Moshi import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.data.Resource @@ -18,7 +17,6 @@ import io.github.wulkanowy.data.enums.MessageFolder.RECEIVED import io.github.wulkanowy.data.mappers.mapFromEntities import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.data.pojos.MessageDraft -import io.github.wulkanowy.data.pojos.MessageDraftJsonAdapter import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.pojo.Folder import io.github.wulkanowy.sdk.pojo.SentMessage @@ -29,6 +27,9 @@ import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.uniqueSubtract import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.sync.Mutex +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json import timber.log.Timber import java.time.LocalDateTime.now import javax.inject.Inject @@ -42,7 +43,7 @@ class MessageRepository @Inject constructor( @ApplicationContext private val context: Context, private val refreshHelper: AutoRefreshHelper, private val sharedPrefProvider: SharedPrefProvider, - private val moshi: Moshi, + private val json: Json, ) { private val saveFetchResultMutex = Mutex() @@ -51,14 +52,18 @@ class MessageRepository @Inject constructor( @Suppress("UNUSED_PARAMETER") fun getMessages( - student: Student, semester: Semester, - folder: MessageFolder, forceRefresh: Boolean, notify: Boolean = false + student: Student, + semester: Semester, + folder: MessageFolder, + forceRefresh: Boolean, + notify: Boolean = false, ): Flow>> = networkBoundResource( mutex = saveFetchResultMutex, shouldFetch = { - it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed( - getRefreshKey(cacheKey, student, folder) + val isExpired = refreshHelper.shouldBeRefreshed( + key = getRefreshKey(cacheKey, student, folder) ) + it.isEmpty() || forceRefresh || isExpired }, query = { messagesDb.loadAll(student.id.toInt(), folder.id) }, fetch = { @@ -77,7 +82,8 @@ class MessageRepository @Inject constructor( ) private fun getMessagesWithReadByChange( - old: List, new: List, + old: List, + new: List, setNotified: Boolean ): List { val oldMeta = old.map { Triple(it, it.readBy, it.unreadBy) } @@ -96,7 +102,9 @@ class MessageRepository @Inject constructor( } fun getMessage( - student: Student, message: Message, markAsRead: Boolean = false + student: Student, + message: Message, + markAsRead: Boolean = false, ): Flow> = networkBoundResource( shouldFetch = { checkNotNull(it, { "This message no longer exist!" }) @@ -135,8 +143,10 @@ class MessageRepository @Inject constructor( } suspend fun sendMessage( - student: Student, subject: String, content: String, - recipients: List + student: Student, + subject: String, + content: String, + recipients: List, ): SentMessage = sdk.init(student).sendMessage( subject = subject, content = content, @@ -159,9 +169,9 @@ class MessageRepository @Inject constructor( var draftMessage: MessageDraft? get() = sharedPrefProvider.getString(context.getString(R.string.pref_key_message_send_draft)) - ?.let { MessageDraftJsonAdapter(moshi).fromJson(it) } + ?.let { json.decodeFromString(it) } set(value) = sharedPrefProvider.putString( context.getString(R.string.pref_key_message_send_draft), - value?.let { MessageDraftJsonAdapter(moshi).toJson(it) } + value?.let { json.encodeToString(it) } ) } 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 4b333bc6..bf17cbbc 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 @@ -28,9 +28,16 @@ class MobileDeviceRepository @Inject constructor( private val cacheKey = "devices" - fun getDevices(student: Student, semester: Semester, forceRefresh: Boolean) = networkBoundResource( + fun getDevices( + student: Student, + semester: Semester, + forceRefresh: Boolean, + ) = networkBoundResource( mutex = saveFetchResultMutex, - shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, student)) }, + shouldFetch = { + val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, student)) + it.isEmpty() || forceRefresh || isExpired + }, query = { mobileDb.loadAll(student.userLoginId.takeIf { it != 0 } ?: student.studentId) }, fetch = { sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) 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 d43cdbc0..c1738b36 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 @@ -12,7 +12,6 @@ import io.github.wulkanowy.utils.init import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.uniqueSubtract import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map import kotlinx.coroutines.sync.Mutex import javax.inject.Inject import javax.inject.Singleton @@ -28,9 +27,19 @@ class NoteRepository @Inject constructor( private val cacheKey = "note" - fun getNotes(student: Student, semester: Semester, forceRefresh: Boolean, notify: Boolean = false) = networkBoundResource( + fun getNotes( + student: Student, + semester: Semester, + forceRefresh: Boolean, + notify: Boolean = false, + ) = networkBoundResource( mutex = saveFetchResultMutex, - shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester)) }, + shouldFetch = { + val isExpired = refreshHelper.shouldBeRefreshed( + getRefreshKey(cacheKey, semester) + ) + it.isEmpty() || forceRefresh || isExpired + }, query = { noteDb.loadAll(student.studentId) }, fetch = { sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/NotificationRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/NotificationRepository.kt new file mode 100644 index 00000000..fca26378 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/NotificationRepository.kt @@ -0,0 +1,17 @@ +package io.github.wulkanowy.data.repositories + +import io.github.wulkanowy.data.db.dao.NotificationDao +import io.github.wulkanowy.data.db.entities.Notification +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class NotificationRepository @Inject constructor( + private val notificationDao: NotificationDao, +) { + + fun getNotifications(studentId: Long) = notificationDao.loadAll(studentId) + + suspend fun saveNotification(notification: Notification) = + notificationDao.insertAll(listOf(notification)) +} diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt index bc8100f2..696ffd63 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt @@ -5,21 +5,23 @@ import android.content.SharedPreferences import androidx.core.content.edit import com.fredporciuncula.flow.preferences.FlowSharedPreferences import com.fredporciuncula.flow.preferences.Preference -import com.squareup.moshi.JsonAdapter -import com.squareup.moshi.Moshi -import com.squareup.moshi.adapter import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.sdk.toLocalDate import io.github.wulkanowy.ui.modules.dashboard.DashboardItem import io.github.wulkanowy.ui.modules.grade.GradeAverageMode +import io.github.wulkanowy.ui.modules.grade.GradeExpandMode import io.github.wulkanowy.ui.modules.grade.GradeSortingMode -import io.github.wulkanowy.utils.toTimestamp import io.github.wulkanowy.utils.toLocalDateTime import io.github.wulkanowy.utils.toTimestamp import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import java.lang.ClassCastException +import java.lang.IllegalStateException import java.time.LocalDate import java.time.LocalDateTime import javax.inject.Inject @@ -28,16 +30,12 @@ import javax.inject.Singleton @OptIn(ExperimentalCoroutinesApi::class) @Singleton class PreferencesRepository @Inject constructor( + @ApplicationContext val context: Context, private val sharedPref: SharedPreferences, private val flowSharedPref: FlowSharedPreferences, - @ApplicationContext val context: Context, - moshi: Moshi + private val json: Json, ) { - @OptIn(ExperimentalStdlibApi::class) - private val dashboardItemsPositionAdapter: JsonAdapter> = - moshi.adapter() - val startMenuIndex: Int get() = getString(R.string.pref_key_start_menu, R.string.pref_default_startup).toInt() @@ -61,8 +59,13 @@ class PreferencesRepository @Inject constructor( R.bool.pref_default_grade_average_force_calc ) - val isGradeExpandable: Boolean - get() = !getBoolean(R.string.pref_key_expand_grade, R.bool.pref_default_expand_grade) + val gradeExpandMode: GradeExpandMode + get() = GradeExpandMode.getByValue( + getString( + R.string.pref_key_expand_grade_mode, + R.string.pref_default_expand_grade_mode + ) + ) val showAllSubjectsOnStatisticsList: Boolean get() = getBoolean( @@ -108,6 +111,22 @@ class PreferencesRepository @Inject constructor( R.bool.pref_default_notification_upcoming_lessons_enable ) + val isUpcomingLessonsNotificationsPersistentKey = + context.getString(R.string.pref_key_notifications_upcoming_lessons_persistent) + val isUpcomingLessonsNotificationsPersistent: Boolean + get() = getBoolean( + isUpcomingLessonsNotificationsPersistentKey, + R.bool.pref_default_notification_upcoming_lessons_persistent + ) + + val isNotificationPiggybackEnabledKey = + context.getString(R.string.pref_key_notifications_piggyback) + val isNotificationPiggybackEnabled: Boolean + get() = getBoolean( + R.string.pref_key_notifications_piggyback, + R.bool.pref_default_notification_piggyback + ) + val isDebugNotificationEnableKey = context.getString(R.string.pref_key_notification_debug) val isDebugNotificationEnable: Boolean get() = getBoolean(isDebugNotificationEnableKey, R.bool.pref_default_notification_debug) @@ -176,22 +195,20 @@ class PreferencesRepository @Inject constructor( ) var lasSyncDate: LocalDateTime - get() = getLong( - R.string.pref_key_last_sync_date, - R.string.pref_default_last_sync_date - ).toLocalDateTime() + get() = getLong(R.string.pref_key_last_sync_date, R.string.pref_default_last_sync_date) + .toLocalDateTime() set(value) = sharedPref.edit().putLong("last_sync_date", value.toTimestamp()).apply() var dashboardItemsPosition: Map? get() { - val json = sharedPref.getString(PREF_KEY_DASHBOARD_ITEMS_POSITION, null) ?: return null + val value = sharedPref.getString(PREF_KEY_DASHBOARD_ITEMS_POSITION, null) ?: return null - return dashboardItemsPositionAdapter.fromJson(json) + return json.decodeFromString(value) } set(value) = sharedPref.edit { putString( PREF_KEY_DASHBOARD_ITEMS_POSITION, - dashboardItemsPositionAdapter.toJson(value) + json.encodeToString(value) ) } @@ -200,6 +217,7 @@ class PreferencesRepository @Inject constructor( .map { set -> set.map { DashboardItem.Tile.valueOf(it) } .plus(DashboardItem.Tile.ACCOUNT) + .plus(DashboardItem.Tile.ADMIN_MESSAGE) .toSet() } @@ -207,6 +225,7 @@ class PreferencesRepository @Inject constructor( get() = selectedDashboardTilesPreference.get() .map { DashboardItem.Tile.valueOf(it) } .plus(DashboardItem.Tile.ACCOUNT) + .plus(DashboardItem.Tile.ADMIN_MESSAGE) .toSet() set(value) { val filteredValue = value.filterNot { it == DashboardItem.Tile.ACCOUNT } @@ -230,8 +249,10 @@ class PreferencesRepository @Inject constructor( set(value) = sharedPref.edit().putInt(PREF_KEY_IN_APP_REVIEW_COUNT, value).apply() var inAppReviewDate: LocalDate? - get() = sharedPref.getLong(PREF_KEY_IN_APP_REVIEW_DATE, 0).takeIf { it != 0L }?.toLocalDate() - set(value) = sharedPref.edit().putLong(PREF_KEY_IN_APP_REVIEW_DATE, value!!.toTimestamp()).apply() + get() = sharedPref.getLong(PREF_KEY_IN_APP_REVIEW_DATE, 0).takeIf { it != 0L } + ?.toLocalDate() + set(value) = sharedPref.edit().putLong(PREF_KEY_IN_APP_REVIEW_DATE, value!!.toTimestamp()) + .apply() var isAppReviewDone: Boolean get() = sharedPref.getBoolean(PREF_KEY_IN_APP_REVIEW_DONE, false) @@ -252,6 +273,9 @@ class PreferencesRepository @Inject constructor( private fun getBoolean(id: String, default: Int) = sharedPref.getBoolean(id, context.resources.getBoolean(default)) + private fun getBoolean(id: Int, default: Boolean) = + sharedPref.getBoolean(context.getString(id), default) + private companion object { private const val PREF_KEY_DASHBOARD_ITEMS_POSITION = "dashboard_items_position" diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/RecipientRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/RecipientRepository.kt index 975a30a2..60e6f248 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/RecipientRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/RecipientRepository.kt @@ -7,6 +7,8 @@ import io.github.wulkanowy.data.db.entities.ReportingUnit import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.sdk.Sdk +import io.github.wulkanowy.utils.AutoRefreshHelper +import io.github.wulkanowy.utils.getRefreshKey import io.github.wulkanowy.utils.init import io.github.wulkanowy.utils.uniqueSubtract import javax.inject.Inject @@ -15,26 +17,34 @@ import javax.inject.Singleton @Singleton class RecipientRepository @Inject constructor( private val recipientDb: RecipientDao, - private val sdk: Sdk + private val sdk: Sdk, + private val refreshHelper: AutoRefreshHelper, ) { + private val cacheKey = "recipient" + suspend fun refreshRecipients(student: Student, unit: ReportingUnit, role: Int) { val new = sdk.init(student).getRecipients(unit.unitId, role).mapToEntities(unit.studentId) val old = recipientDb.loadAll(unit.studentId, unit.unitId, role) recipientDb.deleteAll(old uniqueSubtract new) recipientDb.insertAll(new uniqueSubtract old) + + refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, student)) } suspend fun getRecipients(student: Student, unit: ReportingUnit, role: Int): List { - return recipientDb.loadAll(unit.studentId, unit.unitId, role).ifEmpty { - refreshRecipients(student, unit, role) + val cached = recipientDb.loadAll(unit.studentId, unit.unitId, role) + val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, student)) + return if (cached.isEmpty() || isExpired) { + refreshRecipients(student, unit, role) recipientDb.loadAll(unit.studentId, unit.unitId, role) - } + } else cached } suspend fun getMessageRecipients(student: Student, message: Message): List { - return sdk.init(student).getMessageRecipients(message.messageId, message.senderId).mapToEntities(student.studentId) + return sdk.init(student).getMessageRecipients(message.messageId, message.senderId) + .mapToEntities(student.studentId) } } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/RecoverRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/RecoverRepository.kt index 5e106355..5940f477 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/RecoverRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/RecoverRepository.kt @@ -11,7 +11,7 @@ class RecoverRepository @Inject constructor(private val sdk: Sdk) { return sdk.getPasswordResetCaptchaCode(host, symbol) } - suspend fun sendRecoverRequest(url: String, symbol: String, email: String, reCaptchaResponse: String): String { - return sdk.sendPasswordResetRequest(url, symbol, email, reCaptchaResponse) - } + suspend fun sendRecoverRequest( + url: String, symbol: String, email: String, reCaptchaResponse: String + ): String = sdk.sendPasswordResetRequest(url, symbol, email, reCaptchaResponse) } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolAnnouncementRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolAnnouncementRepository.kt index 62d806ac..b6724ed3 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolAnnouncementRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolAnnouncementRepository.kt @@ -2,7 +2,6 @@ package io.github.wulkanowy.data.repositories import io.github.wulkanowy.data.db.dao.SchoolAnnouncementDao 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.mappers.mapToEntities import io.github.wulkanowy.sdk.Sdk @@ -12,7 +11,6 @@ import io.github.wulkanowy.utils.init import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.uniqueSubtract import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map import kotlinx.coroutines.sync.Mutex import javax.inject.Inject import javax.inject.Singleton @@ -30,17 +28,15 @@ class SchoolAnnouncementRepository @Inject constructor( fun getSchoolAnnouncements( student: Student, - forceRefresh: Boolean, - notify: Boolean = false + forceRefresh: Boolean, notify: Boolean = false ) = networkBoundResource( mutex = saveFetchResultMutex, shouldFetch = { - it.isEmpty() || forceRefresh - || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, student)) + val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, student)) + it.isEmpty() || forceRefresh || isExpired }, query = { - schoolAnnouncementDb.loadAll( - student.studentId) + schoolAnnouncementDb.loadAll(student.studentId) }, fetch = { sdk.init(student) @@ -57,9 +53,11 @@ class SchoolAnnouncementRepository @Inject constructor( refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, student)) } ) + fun getSchoolAnnouncementFromDatabase(student: Student): Flow> { return schoolAnnouncementDb.loadAll(student.studentId) } - suspend fun updateSchoolAnnouncement(schoolAnnouncement: List) = schoolAnnouncementDb.updateAll(schoolAnnouncement) + suspend fun updateSchoolAnnouncement(schoolAnnouncement: List) = + schoolAnnouncementDb.updateAll(schoolAnnouncement) } 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 8b59cb58..288a1fb6 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 @@ -5,6 +5,8 @@ import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.mappers.mapToEntity import io.github.wulkanowy.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.networkBoundResource import kotlinx.coroutines.sync.Mutex @@ -14,29 +16,41 @@ import javax.inject.Singleton @Singleton class SchoolRepository @Inject constructor( private val schoolDb: SchoolDao, - private val sdk: Sdk + private val sdk: Sdk, + private val refreshHelper: AutoRefreshHelper, ) { private val saveFetchResultMutex = Mutex() - fun getSchoolInfo(student: Student, semester: Semester, forceRefresh: Boolean) = - networkBoundResource( - mutex = saveFetchResultMutex, - shouldFetch = { it == null || forceRefresh }, - query = { schoolDb.load(semester.studentId, semester.classId) }, - fetch = { - sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear).getSchool() - .mapToEntity(semester) - }, - saveFetchResult = { old, new -> - if (old != null && new != old) { - with(schoolDb) { - deleteAll(listOf(old)) - insertAll(listOf(new)) - } - } else if (old == null) { - schoolDb.insertAll(listOf(new)) + private val cacheKey = "school_info" + + fun getSchoolInfo( + student: Student, + semester: Semester, + forceRefresh: Boolean, + ) = networkBoundResource( + mutex = saveFetchResultMutex, + shouldFetch = { + val isExpired = refreshHelper.shouldBeRefreshed( + key = getRefreshKey(cacheKey, student) + ) + it == null || forceRefresh || isExpired + }, + query = { schoolDb.load(semester.studentId, semester.classId) }, + fetch = { + sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear).getSchool() + .mapToEntity(semester) + }, + saveFetchResult = { old, new -> + if (old != null && new != old) { + with(schoolDb) { + deleteAll(listOf(old)) + insertAll(listOf(new)) } + } else if (old == null) { + schoolDb.insertAll(listOf(new)) } - ) + refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, student)) + } + ) } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/SemesterRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/SemesterRepository.kt index 4336877a..cc954558 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/SemesterRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/SemesterRepository.kt @@ -26,7 +26,7 @@ class SemesterRepository @Inject constructor( student: Student, forceRefresh: Boolean = false, refreshOnNoCurrent: Boolean = false - ) = withContext(dispatchers.backgroundThread) { + ) = withContext(dispatchers.io) { val semesters = semesterDb.loadAll(student.studentId, student.classId) if (isShouldFetch(student, semesters, forceRefresh, refreshOnNoCurrent)) { @@ -64,7 +64,7 @@ class SemesterRepository @Inject constructor( } suspend fun getCurrentSemester(student: Student, forceRefresh: Boolean = false) = - withContext(dispatchers.backgroundThread) { + withContext(dispatchers.io) { getSemesters(student, forceRefresh).getCurrentOrLast() } } 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 de66ad20..e98daedf 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 @@ -19,24 +19,27 @@ class StudentInfoRepository @Inject constructor( private val saveFetchResultMutex = Mutex() - fun getStudentInfo(student: Student, semester: Semester, forceRefresh: Boolean) = - networkBoundResource( - mutex = saveFetchResultMutex, - shouldFetch = { it == null || forceRefresh }, - query = { studentInfoDao.loadStudentInfo(student.studentId) }, - fetch = { - sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) - .getStudentInfo().mapToEntity(semester) - }, - saveFetchResult = { old, new -> - if (old != null && new != old) { - with(studentInfoDao) { - deleteAll(listOf(old)) - insertAll(listOf(new)) - } - } else if (old == null) { - studentInfoDao.insertAll(listOf(new)) + fun getStudentInfo( + student: Student, + semester: Semester, + forceRefresh: Boolean, + ) = networkBoundResource( + mutex = saveFetchResultMutex, + shouldFetch = { it == null || forceRefresh }, + query = { studentInfoDao.loadStudentInfo(student.studentId) }, + fetch = { + sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) + .getStudentInfo().mapToEntity(semester) + }, + saveFetchResult = { old, new -> + if (old != null && new != old) { + with(studentInfoDao) { + deleteAll(listOf(old)) + insertAll(listOf(new)) } + } else if (old == null) { + studentInfoDao.insertAll(listOf(new)) } - ) + } + ) } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt index c2f364b3..9e4a1aab 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,7 +1,9 @@ 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 import io.github.wulkanowy.data.db.entities.Student @@ -25,7 +27,8 @@ class StudentRepository @Inject constructor( private val studentDb: StudentDao, private val semesterDb: SemesterDao, private val sdk: Sdk, - private val appInfo: AppInfo + private val appInfo: AppInfo, + private val appDatabase: AppDatabase ) { suspend fun isStudentSaved() = getSavedStudents(false).isNotEmpty() @@ -63,7 +66,7 @@ class StudentRepository @Inject constructor( .map { it.apply { if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API) { - student.password = withContext(dispatchers.backgroundThread) { + student.password = withContext(dispatchers.io) { decrypt(student.password) } } @@ -74,7 +77,7 @@ class StudentRepository @Inject constructor( val student = studentDb.loadById(id) ?: throw NoCurrentStudentException() if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API) { - student.password = withContext(dispatchers.backgroundThread) { + student.password = withContext(dispatchers.io) { decrypt(student.password) } } @@ -85,35 +88,40 @@ class StudentRepository @Inject constructor( val student = studentDb.loadCurrent() ?: throw NoCurrentStudentException() if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API) { - student.password = withContext(dispatchers.backgroundThread) { + student.password = withContext(dispatchers.io) { decrypt(student.password) } } return student } - suspend fun saveStudents(studentsWithSemesters: List): List { + suspend fun saveStudents(studentsWithSemesters: List) { val semesters = studentsWithSemesters.flatMap { it.semesters } val students = studentsWithSemesters.map { it.student } .map { it.apply { if (Sdk.Mode.valueOf(it.loginMode) != Sdk.Mode.API) { - password = withContext(dispatchers.backgroundThread) { + password = withContext(dispatchers.io) { encrypt(password, context) } } } } + .mapIndexed { index, student -> + if (index == 0) { + student.copy(isCurrent = true).apply { avatarColor = student.avatarColor } + } else student + } - semesterDb.insertSemesters(semesters) - return studentDb.insertAll(students) + appDatabase.withTransaction { + studentDb.resetCurrent() + semesterDb.insertSemesters(semesters) + studentDb.insertAll(students) + } } suspend fun switchStudent(studentWithSemesters: StudentWithSemesters) { - with(studentDb) { - resetCurrent() - updateCurrent(studentWithSemesters.student.id) - } + studentDb.switchCurrent(studentWithSemesters.student.id) } suspend fun logoutStudent(student: Student) = studentDb.delete(student) 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 b4bfef18..d81cb7c9 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 @@ -5,6 +5,8 @@ import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.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.networkBoundResource import io.github.wulkanowy.utils.uniqueSubtract @@ -15,14 +17,24 @@ import javax.inject.Singleton @Singleton class SubjectRepository @Inject constructor( private val subjectDao: SubjectDao, - private val sdk: Sdk + private val sdk: Sdk, + private val refreshHelper: AutoRefreshHelper, ) { private val saveFetchResultMutex = Mutex() - fun getSubjects(student: Student, semester: Semester, forceRefresh: Boolean = false) = networkBoundResource( + private val cacheKey = "subjects" + + fun getSubjects( + student: Student, + semester: Semester, + forceRefresh: Boolean = false, + ) = networkBoundResource( mutex = saveFetchResultMutex, - shouldFetch = { it.isEmpty() || forceRefresh }, + shouldFetch = { + val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, semester)) + it.isEmpty() || forceRefresh || isExpired + }, query = { subjectDao.loadAll(semester.diaryId, semester.studentId) }, fetch = { sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) @@ -31,6 +43,8 @@ class SubjectRepository @Inject constructor( saveFetchResult = { old, new -> subjectDao.deleteAll(old uniqueSubtract new) subjectDao.insertAll(new uniqueSubtract old) + + refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester)) } ) } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/TeacherRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/TeacherRepository.kt index 7135edbe..029b2707 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 @@ -5,6 +5,8 @@ import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.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.networkBoundResource import io.github.wulkanowy.utils.uniqueSubtract @@ -15,14 +17,24 @@ import javax.inject.Singleton @Singleton class TeacherRepository @Inject constructor( private val teacherDb: TeacherDao, - private val sdk: Sdk + private val sdk: Sdk, + private val refreshHelper: AutoRefreshHelper, ) { private val saveFetchResultMutex = Mutex() - fun getTeachers(student: Student, semester: Semester, forceRefresh: Boolean) = networkBoundResource( + private val cacheKey = "teachers" + + fun getTeachers( + student: Student, + semester: Semester, + forceRefresh: Boolean, + ) = networkBoundResource( mutex = saveFetchResultMutex, - shouldFetch = { it.isEmpty() || forceRefresh }, + shouldFetch = { + val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, semester)) + it.isEmpty() || forceRefresh || isExpired + }, query = { teacherDb.loadAll(semester.studentId, semester.classId) }, fetch = { sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) @@ -32,6 +44,8 @@ class TeacherRepository @Inject constructor( saveFetchResult = { old, new -> teacherDb.deleteAll(old uniqueSubtract new) teacherDb.insertAll(new uniqueSubtract old) + + refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester)) } ) } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/TimetableRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/TimetableRepository.kt index 5495d077..8be62112 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 @@ -41,18 +41,23 @@ class TimetableRepository @Inject constructor( private val cacheKey = "timetable" fun getTimetable( - student: Student, semester: Semester, start: LocalDate, end: LocalDate, - forceRefresh: Boolean, refreshAdditional: Boolean = false + student: Student, + semester: Semester, + start: LocalDate, + end: LocalDate, + forceRefresh: Boolean, + refreshAdditional: Boolean = false, + notify: Boolean = false ) = networkBoundResource( mutex = saveFetchResultMutex, shouldFetch = { (timetable, additional, headers) -> val refreshKey = getRefreshKey(cacheKey, semester, start, end) - val isShouldRefresh = refreshHelper.isShouldBeRefreshed(refreshKey) + val isExpired = refreshHelper.shouldBeRefreshed(refreshKey) val isRefreshAdditional = additional.isEmpty() && refreshAdditional val isNoData = timetable.isEmpty() || isRefreshAdditional || headers.isEmpty() - isNoData || forceRefresh || isShouldRefresh + isNoData || forceRefresh || isExpired }, query = { getFullTimetableFromDatabase(student, semester, start, end) }, fetch = { @@ -63,7 +68,7 @@ class TimetableRepository @Inject constructor( timetableFull.mapToEntities(semester) }, saveFetchResult = { timetableOld, timetableNew -> - refreshTimetable(student, timetableOld.lessons, timetableNew.lessons) + refreshTimetable(student, timetableOld.lessons, timetableNew.lessons, notify) refreshAdditional(timetableOld.additional, timetableNew.additional) refreshDayHeaders(timetableOld.headers, timetableNew.headers) @@ -79,8 +84,10 @@ class TimetableRepository @Inject constructor( ) private fun getFullTimetableFromDatabase( - student: Student, semester: Semester, - start: LocalDate, end: LocalDate + student: Student, + semester: Semester, + start: LocalDate, + end: LocalDate, ): Flow { val timetableFlow = timetableDb.loadAll( diaryId = semester.diaryId, @@ -111,21 +118,27 @@ class TimetableRepository @Inject constructor( } } + fun getTimetableFromDatabase( + semester: Semester, + from: LocalDate, + end: LocalDate + ): Flow> { + return timetableDb.loadAll(semester.diaryId, semester.studentId, from, end) + } + + suspend fun updateTimetable(timetable: List) { + return timetableDb.updateAll(timetable) + } + private suspend fun refreshTimetable( student: Student, - lessonsOld: List, lessonsNew: List + lessonsOld: List, + lessonsNew: List, + notify: Boolean ) { val lessonsToRemove = lessonsOld uniqueSubtract lessonsNew val lessonsToAdd = (lessonsNew uniqueSubtract lessonsOld).map { new -> - val matchingOld = lessonsOld.singleOrNull { new.start == it.start } - if (matchingOld != null) { - val useOldTeacher = new.teacher.isEmpty() && !new.changes && !matchingOld.changes - new.copy( - room = if (new.room.isEmpty()) matchingOld.room else new.room, - teacher = if (useOldTeacher) matchingOld.teacher - else new.teacher - ) - } else new + new.apply { if (notify) isNotified = false } } timetableDb.deleteAll(lessonsToRemove) diff --git a/app/src/main/java/io/github/wulkanowy/services/ServicesModule.kt b/app/src/main/java/io/github/wulkanowy/services/ServicesModule.kt index cdf0c26a..1729f100 100644 --- a/app/src/main/java/io/github/wulkanowy/services/ServicesModule.kt +++ b/app/src/main/java/io/github/wulkanowy/services/ServicesModule.kt @@ -15,6 +15,7 @@ import dagger.multibindings.IntoSet import io.github.wulkanowy.services.sync.channels.Channel import io.github.wulkanowy.services.sync.channels.DebugChannel import io.github.wulkanowy.services.sync.channels.LuckyNumberChannel +import io.github.wulkanowy.services.sync.channels.NewAttendanceChannel import io.github.wulkanowy.services.sync.channels.NewConferencesChannel import io.github.wulkanowy.services.sync.channels.NewExamChannel import io.github.wulkanowy.services.sync.channels.NewGradesChannel @@ -23,6 +24,7 @@ import io.github.wulkanowy.services.sync.channels.NewMessagesChannel import io.github.wulkanowy.services.sync.channels.NewNotesChannel import io.github.wulkanowy.services.sync.channels.NewSchoolAnnouncementsChannel import io.github.wulkanowy.services.sync.channels.PushChannel +import io.github.wulkanowy.services.sync.channels.TimetableChangeChannel import io.github.wulkanowy.services.sync.channels.UpcomingLessonsChannel import io.github.wulkanowy.services.sync.works.AttendanceSummaryWork import io.github.wulkanowy.services.sync.works.AttendanceWork @@ -167,4 +169,12 @@ abstract class ServicesModule { @Binds @IntoSet abstract fun provideUpcomingLessonsChannel(channel: UpcomingLessonsChannel): Channel + + @Binds + @IntoSet + abstract fun provideChangeTimetableChannel(channel: TimetableChangeChannel): Channel + + @Binds + @IntoSet + abstract fun provideNewAttendanceChannel(channel: NewAttendanceChannel): Channel } diff --git a/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationReceiver.kt b/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationReceiver.kt index 406d91f5..b388d2ac 100644 --- a/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationReceiver.kt +++ b/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationReceiver.kt @@ -1,7 +1,6 @@ package io.github.wulkanowy.services.alarm import android.app.PendingIntent -import android.app.PendingIntent.FLAG_UPDATE_CURRENT import android.content.Context import android.content.Intent import android.os.Build @@ -11,11 +10,13 @@ import androidx.core.app.NotificationManagerCompat import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.data.Status +import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.services.HiltBroadcastReceiver import io.github.wulkanowy.services.sync.channels.UpcomingLessonsChannel.Companion.CHANNEL_ID -import io.github.wulkanowy.ui.modules.main.MainActivity -import io.github.wulkanowy.ui.modules.main.MainView +import io.github.wulkanowy.ui.modules.Destination +import io.github.wulkanowy.ui.modules.splash.SplashActivity +import io.github.wulkanowy.utils.PendingIntentCompat import io.github.wulkanowy.utils.flowWithResource import io.github.wulkanowy.utils.getCompatColor import io.github.wulkanowy.utils.toLocalDateTime @@ -32,12 +33,15 @@ class TimetableNotificationReceiver : HiltBroadcastReceiver() { @Inject lateinit var studentRepository: StudentRepository + @Inject + lateinit var preferencesRepository: PreferencesRepository + companion object { const val NOTIFICATION_TYPE_CURRENT = 1 const val NOTIFICATION_TYPE_UPCOMING = 2 const val NOTIFICATION_TYPE_LAST_LESSON_CANCELLATION = 3 - const val NOTIFICATION_ID = "id" + const val NOTIFICATION_ID = 2137 const val STUDENT_NAME = "student_name" const val STUDENT_ID = "student_id" @@ -67,10 +71,10 @@ class TimetableNotificationReceiver : HiltBroadcastReceiver() { private fun prepareNotification(context: Context, intent: Intent) { val type = intent.getIntExtra(LESSON_TYPE, 0) - val notificationId = intent.getIntExtra(NOTIFICATION_ID, MainView.Section.TIMETABLE.id) + val isPersistent = preferencesRepository.isUpcomingLessonsNotificationsPersistent if (type == NOTIFICATION_TYPE_LAST_LESSON_CANCELLATION) { - return NotificationManagerCompat.from(context).cancel(notificationId) + return NotificationManagerCompat.from(context).cancel(NOTIFICATION_ID) } val studentId = intent.getIntExtra(STUDENT_ID, 0) @@ -87,33 +91,58 @@ class TimetableNotificationReceiver : HiltBroadcastReceiver() { Timber.d("TimetableNotification receive: type: $type, subject: $subject, start: ${start.toLocalDateTime()}, student: $studentId") - showNotification(context, notificationId, studentName, + showNotification( + context, isPersistent, studentName, if (type == NOTIFICATION_TYPE_CURRENT) end else start, end - start, - context.getString(if (type == NOTIFICATION_TYPE_CURRENT) R.string.timetable_now else R.string.timetable_next, "($room) $subject".removePrefix("()")), - nextSubject?.let { context.getString(R.string.timetable_later, "($nextRoom) $nextSubject".removePrefix("()")) } + context.getString( + if (type == NOTIFICATION_TYPE_CURRENT) R.string.timetable_now else R.string.timetable_next, + "($room) $subject".removePrefix("()") + ), + nextSubject?.let { + context.getString( + R.string.timetable_later, + "($nextRoom) $nextSubject".removePrefix("()") + ) + } ) } - private fun showNotification(context: Context, notificationId: Int, studentName: String?, countDown: Long, timeout: Long, title: String, next: String?) { - NotificationManagerCompat.from(context).notify(notificationId, NotificationCompat.Builder(context, CHANNEL_ID) - .setContentTitle(title) - .setContentText(next) - .setAutoCancel(false) - .setOngoing(true) - .setWhen(countDown) - .apply { - if (Build.VERSION.SDK_INT >= N) setUsesChronometer(true) - } - .setTimeoutAfter(timeout) - .setSmallIcon(R.drawable.ic_stat_timetable) - .setColor(context.getCompatColor(R.color.colorPrimary)) - .setStyle(NotificationCompat.InboxStyle().also { - it.setSummaryText(studentName) - it.addLine(next) - }) - .setContentIntent(PendingIntent.getActivity(context, MainView.Section.TIMETABLE.id, - MainActivity.getStartIntent(context, MainView.Section.TIMETABLE, true), FLAG_UPDATE_CURRENT)) - .build() - ) + private fun showNotification( + context: Context, + isPersistent: Boolean, + studentName: String?, + countDown: Long, + timeout: Long, + title: String, + next: String? + ) { + NotificationManagerCompat.from(context) + .notify(NOTIFICATION_ID, NotificationCompat.Builder(context, CHANNEL_ID) + .setContentTitle(title) + .setContentText(next) + .setAutoCancel(false) + .setWhen(countDown) + .setOngoing(isPersistent) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + .apply { + if (Build.VERSION.SDK_INT >= N) setUsesChronometer(true) + } + .setTimeoutAfter(timeout) + .setSmallIcon(R.drawable.ic_stat_timetable) + .setColor(context.getCompatColor(R.color.colorPrimary)) + .setStyle(NotificationCompat.InboxStyle().also { + it.setSummaryText(studentName) + it.addLine(next) + }) + .setContentIntent( + PendingIntent.getActivity( + context, + NOTIFICATION_ID, + SplashActivity.getStartIntent(context, Destination.Timetable()), + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE + ) + ) + .build() + ) } } 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 98bd93eb..6e39b910 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 @@ -3,7 +3,6 @@ package io.github.wulkanowy.services.alarm import android.app.AlarmManager import android.app.AlarmManager.RTC_WAKEUP import android.app.PendingIntent -import android.app.PendingIntent.FLAG_UPDATE_CURRENT import android.content.Context import android.content.Intent import androidx.core.app.AlarmManagerCompat @@ -25,12 +24,13 @@ import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companio import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companion.NOTIFICATION_TYPE_UPCOMING import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companion.STUDENT_ID import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companion.STUDENT_NAME -import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.utils.DispatchersProvider +import io.github.wulkanowy.utils.PendingIntentCompat import io.github.wulkanowy.utils.nickOrName import io.github.wulkanowy.utils.toTimestamp import kotlinx.coroutines.withContext import timber.log.Timber +import java.time.LocalDate import java.time.LocalDateTime import java.time.LocalDateTime.now import javax.inject.Inject @@ -53,14 +53,17 @@ class TimetableNotificationSchedulerHelper @Inject constructor( suspend fun cancelScheduled(lessons: List, student: Student) { val studentId = student.studentId - withContext(dispatchersProvider.backgroundThread) { + withContext(dispatchersProvider.io) { lessons.sortedBy { it.start }.forEachIndexed { index, lesson -> val upcomingTime = getUpcomingLessonTime(index, lessons, lesson) cancelScheduledTo( - upcomingTime..lesson.start, - getRequestCode(upcomingTime, studentId) + range = upcomingTime..lesson.start, + requestCode = getRequestCode(upcomingTime, studentId) + ) + cancelScheduledTo( + range = lesson.start..lesson.end, + requestCode = getRequestCode(lesson.start, studentId) ) - cancelScheduledTo(lesson.start..lesson.end, getRequestCode(lesson.start, studentId)) Timber.d("TimetableNotification canceled: type 1 & 2, subject: ${lesson.subject}, start: ${lesson.start}, student: $studentId") } @@ -69,20 +72,31 @@ class TimetableNotificationSchedulerHelper @Inject constructor( private fun cancelScheduledTo(range: ClosedRange, requestCode: Int) { if (now() in range) cancelNotification() + alarmManager.cancel( - PendingIntent.getBroadcast(context, requestCode, Intent(), FLAG_UPDATE_CURRENT) + PendingIntent.getBroadcast( + context, + requestCode, + Intent(), + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE + ) ) } fun cancelNotification() = - NotificationManagerCompat.from(context).cancel(MainView.Section.TIMETABLE.id) + NotificationManagerCompat.from(context).cancel(NOTIFICATION_ID) suspend fun scheduleNotifications(lessons: List, student: Student) { if (!preferencesRepository.isUpcomingLessonsNotificationsEnable) { return cancelScheduled(lessons, student) } - withContext(dispatchersProvider.backgroundThread) { + if (lessons.firstOrNull()?.date?.isAfter(LocalDate.now().plusDays(2)) == true) { + Timber.d("Timetable notification scheduling skipped - lessons are too far") + return + } + + withContext(dispatchersProvider.io) { lessons.groupBy { it.date } .map { it.value.sortedBy { lesson -> lesson.start } } .map { it.filter { lesson -> lesson.isStudentPlan } } @@ -96,26 +110,26 @@ class TimetableNotificationSchedulerHelper @Inject constructor( if (lesson.start > now()) { scheduleBroadcast( - intent, - student.studentId, - NOTIFICATION_TYPE_UPCOMING, - getUpcomingLessonTime(index, active, lesson) + intent = intent, + studentId = student.studentId, + notificationType = NOTIFICATION_TYPE_UPCOMING, + time = getUpcomingLessonTime(index, active, lesson) ) } if (lesson.end > now()) { scheduleBroadcast( - intent, - student.studentId, - NOTIFICATION_TYPE_CURRENT, - lesson.start + intent = intent, + studentId = student.studentId, + notificationType = NOTIFICATION_TYPE_CURRENT, + time = lesson.start ) if (active.lastIndex == index) { scheduleBroadcast( - intent, - student.studentId, - NOTIFICATION_TYPE_LAST_LESSON_CANCELLATION, - lesson.end + intent = intent, + studentId = student.studentId, + notificationType = NOTIFICATION_TYPE_LAST_LESSON_CANCELLATION, + time = lesson.end ) } } @@ -143,17 +157,20 @@ class TimetableNotificationSchedulerHelper @Inject constructor( notificationType: Int, time: LocalDateTime ) { - AlarmManagerCompat.setExactAndAllowWhileIdle( - alarmManager, RTC_WAKEUP, time.toTimestamp(), - PendingIntent.getBroadcast(context, getRequestCode(time, studentId), intent.also { - it.putExtra(NOTIFICATION_ID, MainView.Section.TIMETABLE.id) - it.putExtra(LESSON_TYPE, notificationType) - }, FLAG_UPDATE_CURRENT) - ) - Timber.d( - "TimetableNotification scheduled: type: $notificationType, subject: ${ - intent.getStringExtra(LESSON_TITLE) - }, start: $time, student: $studentId" - ) + try { + AlarmManagerCompat.setExactAndAllowWhileIdle( + alarmManager, RTC_WAKEUP, time.toTimestamp(), + PendingIntent.getBroadcast(context, getRequestCode(time, studentId), intent.also { + it.putExtra(LESSON_TYPE, notificationType) + }, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE) + ) + Timber.d( + "TimetableNotification scheduled: type: $notificationType, subject: ${ + intent.getStringExtra(LESSON_TITLE) + }, start: $time, student: $studentId" + ) + } catch (e: IllegalStateException) { + Timber.e(e) + } } } diff --git a/app/src/main/java/io/github/wulkanowy/services/piggyback/VulcanNotificationListenerService.kt b/app/src/main/java/io/github/wulkanowy/services/piggyback/VulcanNotificationListenerService.kt new file mode 100644 index 00000000..c7df2dbc --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/services/piggyback/VulcanNotificationListenerService.kt @@ -0,0 +1,24 @@ +package io.github.wulkanowy.services.piggyback + +import android.service.notification.NotificationListenerService +import android.service.notification.StatusBarNotification +import dagger.hilt.android.AndroidEntryPoint +import io.github.wulkanowy.data.repositories.PreferencesRepository +import io.github.wulkanowy.services.sync.SyncManager +import javax.inject.Inject + +@AndroidEntryPoint +class VulcanNotificationListenerService : NotificationListenerService() { + + @Inject + lateinit var syncManager: SyncManager + + @Inject + lateinit var preferenceRepository: PreferencesRepository + + override fun onNotificationPosted(statusBarNotification: StatusBarNotification?) { + if (statusBarNotification?.packageName == "pl.edu.vulcan.hebe" && preferenceRepository.isNotificationPiggybackEnabled) { + syncManager.startOneTimeSyncWorker() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/services/shortcuts/ShortcutsHelper.kt b/app/src/main/java/io/github/wulkanowy/services/shortcuts/ShortcutsHelper.kt new file mode 100644 index 00000000..4ad9ac12 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/services/shortcuts/ShortcutsHelper.kt @@ -0,0 +1,90 @@ +package io.github.wulkanowy.services.shortcuts + +import android.content.Context +import android.content.Intent +import androidx.core.content.pm.ShortcutInfoCompat +import androidx.core.content.pm.ShortcutManagerCompat +import androidx.core.graphics.drawable.IconCompat +import dagger.hilt.android.qualifiers.ApplicationContext +import io.github.wulkanowy.R +import io.github.wulkanowy.ui.modules.Destination +import io.github.wulkanowy.ui.modules.splash.SplashActivity +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class ShortcutsHelper @Inject constructor(@ApplicationContext private val context: Context) { + + private val destinations = mapOf( + "grade" to Destination.Grade, + "attendance" to Destination.Attendance, + "exam" to Destination.Exam, + "timetable" to Destination.Timetable() + ) + + init { + initializeShortcuts() + } + + fun getDestination(intent: Intent) = + destinations[intent.getStringExtra(EXTRA_SHORTCUT_DESTINATION_ID)] + + private fun initializeShortcuts() { + val shortcutsInfo = listOf( + ShortcutInfoCompat.Builder(context, "grade_shortcut") + .setShortLabel(context.getString(R.string.grade_title)) + .setLongLabel(context.getString(R.string.grade_title)) + .setIcon(IconCompat.createWithResource(context, R.drawable.ic_shortcut_grade)) + .setIntent(SplashActivity.getStartIntent(context) + .apply { + action = Intent.ACTION_VIEW + putExtra(EXTRA_SHORTCUT_DESTINATION_ID, "grade") + } + ) + .build(), + + ShortcutInfoCompat.Builder(context, "attendance_shortcut") + .setShortLabel(context.getString(R.string.attendance_title)) + .setLongLabel(context.getString(R.string.attendance_title)) + .setIcon(IconCompat.createWithResource(context, R.drawable.ic_shortcut_attendance)) + .setIntent(SplashActivity.getStartIntent(context) + .apply { + action = Intent.ACTION_VIEW + putExtra(EXTRA_SHORTCUT_DESTINATION_ID, "attendance") + } + ) + .build(), + + ShortcutInfoCompat.Builder(context, "exam_shortcut") + .setShortLabel(context.getString(R.string.exam_title)) + .setLongLabel(context.getString(R.string.exam_title)) + .setIcon(IconCompat.createWithResource(context, R.drawable.ic_shortcut_exam)) + .setIntent(SplashActivity.getStartIntent(context) + .apply { + action = Intent.ACTION_VIEW + putExtra(EXTRA_SHORTCUT_DESTINATION_ID, "exam") + } + ) + .build(), + + ShortcutInfoCompat.Builder(context, "timetable_shortcut") + .setShortLabel(context.getString(R.string.timetable_title)) + .setLongLabel(context.getString(R.string.timetable_title)) + .setIcon(IconCompat.createWithResource(context, R.drawable.ic_shortcut_timetable)) + .setIntent(SplashActivity.getStartIntent(context) + .apply { + action = Intent.ACTION_VIEW + putExtra(EXTRA_SHORTCUT_DESTINATION_ID, "timetable") + } + ) + .build() + ) + + shortcutsInfo.forEach { ShortcutManagerCompat.pushDynamicShortcut(context, it) } + } + + private companion object { + + private const val EXTRA_SHORTCUT_DESTINATION_ID = "shortcut_destination_id" + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/SyncManager.kt b/app/src/main/java/io/github/wulkanowy/services/sync/SyncManager.kt index b94d97e3..32ca20af 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/SyncManager.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/SyncManager.kt @@ -57,18 +57,24 @@ class SyncManager @Inject constructor( fun startPeriodicSyncWorker(restart: Boolean = false) { if (preferencesRepository.isServiceEnabled && !now().isHolidays) { - workManager.enqueueUniquePeriodicWork(SyncWorker::class.java.simpleName, if (restart) REPLACE else KEEP, - PeriodicWorkRequestBuilder(preferencesRepository.servicesInterval, MINUTES) + val serviceInterval = preferencesRepository.servicesInterval + + workManager.enqueueUniquePeriodicWork( + SyncWorker::class.java.simpleName, if (restart) REPLACE else KEEP, + PeriodicWorkRequestBuilder(serviceInterval, MINUTES) .setInitialDelay(10, MINUTES) .setBackoffCriteria(EXPONENTIAL, 30, MINUTES) - .setConstraints(Constraints.Builder() - .setRequiredNetworkType(if (preferencesRepository.isServicesOnlyWifi) UNMETERED else CONNECTED) - .build()) - .build()) + .setConstraints( + Constraints.Builder() + .setRequiredNetworkType(if (preferencesRepository.isServicesOnlyWifi) UNMETERED else CONNECTED) + .build() + ) + .build() + ) } } - fun startOneTimeSyncWorker(): Flow { + fun startOneTimeSyncWorker(): Flow { val work = OneTimeWorkRequestBuilder() .setInputData( Data.Builder() @@ -77,7 +83,11 @@ class SyncManager @Inject constructor( ) .build() - workManager.enqueueUniqueWork("${SyncWorker::class.java.simpleName}_one_time", ExistingWorkPolicy.REPLACE, work) + workManager.enqueueUniqueWork( + "${SyncWorker::class.java.simpleName}_one_time", + ExistingWorkPolicy.REPLACE, + work + ) return workManager.getWorkInfoByIdLiveData(work.id).asFlow() } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/SyncWorker.kt b/app/src/main/java/io/github/wulkanowy/services/sync/SyncWorker.kt index ea1f79cb..52979e63 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/SyncWorker.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/SyncWorker.kt @@ -19,11 +19,11 @@ import io.github.wulkanowy.sdk.exception.FeatureNotAvailableException import io.github.wulkanowy.sdk.scrapper.exception.FeatureDisabledException import io.github.wulkanowy.services.sync.channels.DebugChannel import io.github.wulkanowy.services.sync.works.Work +import io.github.wulkanowy.utils.DispatchersProvider import io.github.wulkanowy.utils.getCompatColor -import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.withContext import timber.log.Timber import java.time.LocalDateTime -import java.time.ZoneId import kotlin.random.Random @HiltWorker @@ -34,13 +34,14 @@ class SyncWorker @AssistedInject constructor( private val semesterRepository: SemesterRepository, private val works: Set<@JvmSuppressWildcards Work>, private val preferencesRepository: PreferencesRepository, - private val notificationManager: NotificationManagerCompat + private val notificationManager: NotificationManagerCompat, + private val dispatchersProvider: DispatchersProvider ) : CoroutineWorker(appContext, workerParameters) { - override suspend fun doWork() = coroutineScope { + override suspend fun doWork() = withContext(dispatchersProvider.io) { Timber.i("SyncWorker is starting") - if (!studentRepository.isCurrentStudentSet()) return@coroutineScope Result.failure() + if (!studentRepository.isCurrentStudentSet()) return@withContext Result.failure() val student = studentRepository.getCurrentStudent() val semester = semesterRepository.getCurrentSemester(student, true) @@ -50,12 +51,12 @@ class SyncWorker @AssistedInject constructor( Timber.i("${work::class.java.simpleName} is starting") work.doWork(student, semester) Timber.i("${work::class.java.simpleName} result: Success") - preferencesRepository.lasSyncDate = LocalDateTime.now(ZoneId.systemDefault()) null } catch (e: Throwable) { Timber.w("${work::class.java.simpleName} result: An exception ${e.message} occurred") - if (e is FeatureDisabledException || e is FeatureNotAvailableException) null - else { + if (e is FeatureDisabledException || e is FeatureNotAvailableException) { + null + } else { Timber.e(e) e } @@ -70,13 +71,16 @@ class SyncWorker @AssistedInject constructor( ) } exceptions.isNotEmpty() -> Result.retry() - else -> Result.success() + else -> { + preferencesRepository.lasSyncDate = LocalDateTime.now() + Result.success() + } } if (preferencesRepository.isDebugNotificationEnable) notify(result) Timber.i("SyncWorker result: $result") - result + return@withContext result } private fun notify(result: Result) { diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/channels/NewAttendanceChannel.kt b/app/src/main/java/io/github/wulkanowy/services/sync/channels/NewAttendanceChannel.kt new file mode 100644 index 00000000..3110099e --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/services/sync/channels/NewAttendanceChannel.kt @@ -0,0 +1,36 @@ +package io.github.wulkanowy.services.sync.channels + +import android.annotation.TargetApi +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.content.Context +import androidx.core.app.NotificationManagerCompat +import dagger.hilt.android.qualifiers.ApplicationContext +import io.github.wulkanowy.R +import javax.inject.Inject + +@TargetApi(26) +class NewAttendanceChannel @Inject constructor( + private val notificationManager: NotificationManagerCompat, + @ApplicationContext private val context: Context +) : Channel { + + companion object { + const val CHANNEL_ID = "new_attendance_channel" + } + + override fun create() { + notificationManager.createNotificationChannel( + NotificationChannel( + CHANNEL_ID, + context.getString(R.string.channel_new_attendance), + NotificationManager.IMPORTANCE_HIGH + ) + .apply { + enableLights(true) + enableVibration(true) + lockscreenVisibility = Notification.VISIBILITY_PUBLIC + }) + } +} diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/channels/TimetableChangeChannel.kt b/app/src/main/java/io/github/wulkanowy/services/sync/channels/TimetableChangeChannel.kt new file mode 100644 index 00000000..10dd3e00 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/services/sync/channels/TimetableChangeChannel.kt @@ -0,0 +1,36 @@ +package io.github.wulkanowy.services.sync.channels + +import android.annotation.TargetApi +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.content.Context +import androidx.core.app.NotificationManagerCompat +import dagger.hilt.android.qualifiers.ApplicationContext +import io.github.wulkanowy.R +import javax.inject.Inject + +@TargetApi(26) +class TimetableChangeChannel @Inject constructor( + private val notificationManager: NotificationManagerCompat, + @ApplicationContext private val context: Context +) : Channel { + + companion object { + const val CHANNEL_ID = "change_timetable_channel" + } + + override fun create() { + notificationManager.createNotificationChannel( + NotificationChannel( + CHANNEL_ID, + context.getString(R.string.channel_change_timetable), + NotificationManager.IMPORTANCE_HIGH + ) + .apply { + enableLights(true) + enableVibration(true) + lockscreenVisibility = Notification.VISIBILITY_PUBLIC + }) + } +} diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/AppNotificationManager.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/AppNotificationManager.kt new file mode 100644 index 00000000..e276df0c --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/AppNotificationManager.kt @@ -0,0 +1,180 @@ +package io.github.wulkanowy.services.sync.notifications + +import android.annotation.SuppressLint +import android.app.PendingIntent +import android.content.Context +import android.os.Build +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import dagger.hilt.android.qualifiers.ApplicationContext +import io.github.wulkanowy.R +import io.github.wulkanowy.data.db.entities.Notification +import io.github.wulkanowy.data.db.entities.Student +import io.github.wulkanowy.data.pojos.GroupNotificationData +import io.github.wulkanowy.data.pojos.NotificationData +import io.github.wulkanowy.data.repositories.NotificationRepository +import io.github.wulkanowy.data.repositories.StudentRepository +import io.github.wulkanowy.utils.PendingIntentCompat +import io.github.wulkanowy.utils.getCompatBitmap +import io.github.wulkanowy.utils.getCompatColor +import io.github.wulkanowy.utils.nickOrName +import java.time.LocalDateTime +import javax.inject.Inject +import kotlin.random.Random + +class AppNotificationManager @Inject constructor( + private val notificationManager: NotificationManagerCompat, + @ApplicationContext private val context: Context, + private val studentRepository: StudentRepository, + private val notificationRepository: NotificationRepository +) { + + @SuppressLint("InlinedApi") + suspend fun sendSingleNotification( + notificationData: NotificationData, + notificationType: NotificationType, + student: Student + ) { + val notification = NotificationCompat.Builder(context, notificationType.channel) + .setLargeIcon(context.getCompatBitmap(notificationType.icon, R.color.colorPrimary)) + .setSmallIcon(R.drawable.ic_stat_all) + .setAutoCancel(true) + .setDefaults(NotificationCompat.DEFAULT_ALL) + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setColor(context.getCompatColor(R.color.colorPrimary)) + .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY) + .setContentIntent( + PendingIntent.getActivity( + context, + Random.nextInt(), + notificationData.intentToStart, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE + ) + ) + .setContentTitle(notificationData.title) + .setContentText(notificationData.content) + .setStyle( + NotificationCompat.BigTextStyle() + .bigText(notificationData.content) + .also { builder -> + if (shouldShowStudentName()) { + builder.setSummaryText(student.nickOrName) + } + } + ) + .build() + + notificationManager.notify(Random.nextInt(), notification) + saveNotification(notificationData, notificationType, student) + } + + @SuppressLint("InlinedApi") + suspend fun sendMultipleNotifications( + groupNotificationData: GroupNotificationData, + student: Student + ) { + val notificationType = groupNotificationData.type + val groupType = notificationType.group ?: return + val group = "${groupType}_${student.id}" + + sendSummaryNotification(groupNotificationData, group, student) + + groupNotificationData.notificationDataList.forEach { notificationData -> + val notification = NotificationCompat.Builder(context, notificationType.channel) + .setLargeIcon(context.getCompatBitmap(notificationType.icon, R.color.colorPrimary)) + .setSmallIcon(R.drawable.ic_stat_all) + .setAutoCancel(true) + .setDefaults(NotificationCompat.DEFAULT_ALL) + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setColor(context.getCompatColor(R.color.colorPrimary)) + .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY) + .setContentIntent( + PendingIntent.getActivity( + context, + Random.nextInt(), + notificationData.intentToStart, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE + ) + ) + .setContentTitle(notificationData.title) + .setContentText(notificationData.content) + .setStyle( + NotificationCompat.BigTextStyle() + .bigText(notificationData.content) + .also { builder -> + if (shouldShowStudentName()) { + builder.setSummaryText(student.nickOrName) + } + } + ) + .setGroup(group) + .build() + + notificationManager.notify(Random.nextInt(), notification) + saveNotification(notificationData, groupNotificationData.type, student) + } + } + + private suspend fun sendSummaryNotification( + groupNotificationData: GroupNotificationData, + group: String, + student: Student + ) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return + + val summaryNotification = + NotificationCompat.Builder(context, groupNotificationData.type.channel) + .setContentTitle(groupNotificationData.title) + .setContentText(groupNotificationData.content) + .setSmallIcon(groupNotificationData.type.icon) + .setAutoCancel(true) + .setDefaults(NotificationCompat.DEFAULT_ALL) + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setColor(context.getCompatColor(R.color.colorPrimary)) + .setStyle( + NotificationCompat.InboxStyle() + .also { builder -> + if (shouldShowStudentName()) { + builder.setSummaryText(student.nickOrName) + } + groupNotificationData.notificationDataList.forEach { + builder.addLine(it.content) + } + } + ) + .setContentIntent( + PendingIntent.getActivity( + context, + Random.nextInt(), + groupNotificationData.intentToStart, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE + ) + ) + .setLocalOnly(true) + .setGroup(group) + .setGroupSummary(true) + .build() + + val groupId = student.id * 100 + groupNotificationData.type.ordinal + notificationManager.notify(groupId.toInt(), summaryNotification) + } + + private suspend fun saveNotification( + notificationData: NotificationData, + notificationType: NotificationType, + student: Student + ) { + val notificationEntity = Notification( + studentId = student.id, + title = notificationData.title, + content = notificationData.content, + type = notificationType, + date = LocalDateTime.now() + ) + + notificationRepository.saveNotification(notificationEntity) + } + + private suspend fun shouldShowStudentName(): Boolean = + studentRepository.getSavedStudents(decryptPass = false).size > 1 +} diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/BaseNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/BaseNotification.kt deleted file mode 100644 index 8c9cb471..00000000 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/BaseNotification.kt +++ /dev/null @@ -1,102 +0,0 @@ -package io.github.wulkanowy.services.sync.notifications - -import android.app.PendingIntent -import android.content.Context -import android.os.Build -import androidx.annotation.PluralsRes -import androidx.core.app.NotificationCompat -import androidx.core.app.NotificationManagerCompat -import io.github.wulkanowy.R -import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.data.pojos.MultipleNotifications -import io.github.wulkanowy.data.pojos.Notification -import io.github.wulkanowy.data.pojos.OneNotification -import io.github.wulkanowy.ui.modules.main.MainActivity -import io.github.wulkanowy.utils.getCompatBitmap -import io.github.wulkanowy.utils.getCompatColor -import io.github.wulkanowy.utils.nickOrName -import kotlin.random.Random - -abstract class BaseNotification( - private val context: Context, - private val notificationManager: NotificationManagerCompat, -) { - - protected fun sendNotification(notification: Notification, student: Student) = - when (notification) { - is OneNotification -> sendOneNotification(notification, student) - is MultipleNotifications -> sendMultipleNotifications(notification, student) - } - - private fun sendOneNotification(notification: OneNotification, student: Student?) { - notificationManager.notify( - Random.nextInt(Int.MAX_VALUE), - getNotificationBuilder(notification).apply { - val content = context.getString( - notification.contentStringRes, - *notification.contentValues.toTypedArray() - ) - setContentTitle(context.getString(notification.titleStringRes)) - setContentText(content) - setStyle( - NotificationCompat.BigTextStyle() - .setSummaryText(student?.nickOrName) - .bigText(content) - ) - }.build() - ) - } - - private fun sendMultipleNotifications(notification: MultipleNotifications, student: Student) { - val group = notification.type.group + student.id - val groupId = student.id * 100 + notification.type.ordinal - - notification.lines.forEach { item -> - notificationManager.notify( - Random.nextInt(Int.MAX_VALUE), - getNotificationBuilder(notification).apply { - setContentTitle(getQuantityString(notification.titleStringRes, 1)) - setContentText(item) - setStyle( - NotificationCompat.BigTextStyle() - .setSummaryText(student.nickOrName) - .bigText(item) - ) - setGroup(group) - }.build() - ) - } - - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return - - notificationManager.notify( - groupId.toInt(), - getNotificationBuilder(notification).apply { - setSmallIcon(notification.icon) - setGroup(group) - setStyle(NotificationCompat.InboxStyle().setSummaryText(student.nickOrName)) - setGroupSummary(true) - }.build() - ) - } - - private fun getNotificationBuilder(notification: Notification) = NotificationCompat - .Builder(context, notification.type.channel) - .setLargeIcon(context.getCompatBitmap(notification.icon, R.color.colorPrimary)) - .setSmallIcon(R.drawable.ic_stat_all) - .setAutoCancel(true) - .setDefaults(NotificationCompat.DEFAULT_ALL) - .setPriority(NotificationCompat.PRIORITY_HIGH) - .setColor(context.getCompatColor(R.color.colorPrimary)) - .setContentIntent( - PendingIntent.getActivity( - context, notification.startMenu.id, - MainActivity.getStartIntent(context, notification.startMenu, true), - PendingIntent.FLAG_UPDATE_CURRENT - ) - ) - - private fun getQuantityString(@PluralsRes id: Int, value: Int): String { - return context.resources.getQuantityString(id, value, value) - } -} diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/ChangeTimetableNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/ChangeTimetableNotification.kt new file mode 100644 index 00000000..7bfef96a --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/ChangeTimetableNotification.kt @@ -0,0 +1,124 @@ +package io.github.wulkanowy.services.sync.notifications + +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext +import io.github.wulkanowy.R +import io.github.wulkanowy.data.db.entities.Student +import io.github.wulkanowy.data.db.entities.Timetable +import io.github.wulkanowy.data.pojos.GroupNotificationData +import io.github.wulkanowy.data.pojos.NotificationData +import io.github.wulkanowy.ui.modules.Destination +import io.github.wulkanowy.ui.modules.splash.SplashActivity +import io.github.wulkanowy.utils.getPlural +import io.github.wulkanowy.utils.toFormattedString +import java.time.LocalDate +import java.time.LocalDateTime +import javax.inject.Inject + +class ChangeTimetableNotification @Inject constructor( + private val appNotificationManager: AppNotificationManager, + @ApplicationContext private val context: Context, +) { + + suspend fun notify(items: List, student: Student) { + val currentTime = LocalDateTime.now() + val changedLessons = items.filter { (it.canceled || it.changes) && it.start > currentTime } + val notificationDataList = changedLessons.groupBy { it.date } + .map { (date, lessons) -> + getNotificationContents(date, lessons).map { + NotificationData( + title = context.getPlural( + R.plurals.timetable_notify_new_items_title, + 1 + ), + content = it, + intentToStart = SplashActivity.getStartIntent( + context = context, + destination = Destination.Timetable(date) + ) + ) + } + } + .flatten() + .ifEmpty { return } + + val groupNotificationData = GroupNotificationData( + notificationDataList = notificationDataList, + title = context.getPlural( + R.plurals.timetable_notify_new_items_title, + changedLessons.size + ), + content = context.getPlural( + R.plurals.timetable_notify_new_items_group, + changedLessons.size, + changedLessons.size + ), + intentToStart = SplashActivity.getStartIntent(context, Destination.Timetable()), + type = NotificationType.CHANGE_TIMETABLE + ) + + appNotificationManager.sendMultipleNotifications(groupNotificationData, student) + } + + private fun getNotificationContents(date: LocalDate, lessons: List): List { + val formattedDate = date.toFormattedString("EEE dd.MM") + + return if (lessons.size > 2) { + listOf( + context.getPlural( + R.plurals.timetable_notify_new_items, + lessons.size, + formattedDate, + lessons.size, + ) + ) + } else { + lessons.map { + buildString { + append( + context.getString( + R.string.timetable_notify_lesson, + formattedDate, + it.number, + it.subject + ) + ) + if (it.roomOld.isNotBlank()) { + appendLine() + append( + context.getString( + R.string.timetable_notify_change_room, + it.roomOld, + it.room + ) + ) + } + if (it.teacherOld.isNotBlank() && it.teacher != it.teacherOld) { + appendLine() + append( + context.getString( + R.string.timetable_notify_change_teacher, + it.teacherOld, + it.teacher + ) + ) + } + if (it.subjectOld.isNotBlank()) { + appendLine() + append( + context.getString( + R.string.timetable_notify_change_subject, + it.subjectOld, + it.subject + ) + ) + } + if (it.info.isNotBlank()) { + appendLine() + append(it.info) + } + } + } + } + } +} diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewAttendanceNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewAttendanceNotification.kt new file mode 100644 index 00000000..c78dcd05 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewAttendanceNotification.kt @@ -0,0 +1,55 @@ +package io.github.wulkanowy.services.sync.notifications + +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext +import io.github.wulkanowy.R +import io.github.wulkanowy.data.db.entities.Attendance +import io.github.wulkanowy.data.db.entities.Student +import io.github.wulkanowy.data.pojos.GroupNotificationData +import io.github.wulkanowy.data.pojos.NotificationData +import io.github.wulkanowy.ui.modules.Destination +import io.github.wulkanowy.ui.modules.splash.SplashActivity +import io.github.wulkanowy.utils.descriptionRes +import io.github.wulkanowy.utils.getPlural +import io.github.wulkanowy.utils.toFormattedString +import javax.inject.Inject + +class NewAttendanceNotification @Inject constructor( + private val appNotificationManager: AppNotificationManager, + @ApplicationContext private val context: Context +) { + + suspend fun notify(items: List, student: Student) { + val lines = items.filterNot { it.presence || it.name == "UNKNOWN" } + .map { + val description = context.getString(it.descriptionRes) + "${it.date.toFormattedString("dd.MM")} - ${it.subject}: $description" + } + .ifEmpty { return } + + val notificationDataList = lines.map { + NotificationData( + title = context.getPlural(R.plurals.attendance_notify_new_items_title, 1), + content = it, + intentToStart = SplashActivity.getStartIntent(context, Destination.Attendance) + ) + } + + val groupNotificationData = GroupNotificationData( + notificationDataList = notificationDataList, + title = context.getPlural( + R.plurals.attendance_notify_new_items_title, + notificationDataList.size + ), + content = context.getPlural( + R.plurals.attendance_notify_new_items, + notificationDataList.size, + notificationDataList.size + ), + intentToStart = SplashActivity.getStartIntent(context, Destination.Attendance), + type = NotificationType.NEW_ATTENDANCE + ) + + appNotificationManager.sendMultipleNotifications(groupNotificationData, student) + } +} diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewConferenceNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewConferenceNotification.kt index fda2922f..8ef14788 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewConferenceNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewConferenceNotification.kt @@ -1,38 +1,52 @@ package io.github.wulkanowy.services.sync.notifications import android.content.Context -import androidx.core.app.NotificationManagerCompat import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Conference import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.data.pojos.MultipleNotifications -import io.github.wulkanowy.ui.modules.main.MainView +import io.github.wulkanowy.data.pojos.GroupNotificationData +import io.github.wulkanowy.data.pojos.NotificationData +import io.github.wulkanowy.ui.modules.Destination +import io.github.wulkanowy.ui.modules.splash.SplashActivity +import io.github.wulkanowy.utils.getPlural import io.github.wulkanowy.utils.toFormattedString import java.time.LocalDateTime import javax.inject.Inject class NewConferenceNotification @Inject constructor( - @ApplicationContext private val context: Context, - notificationManager: NotificationManagerCompat, -) : BaseNotification(context, notificationManager) { + private val appNotificationManager: AppNotificationManager, + @ApplicationContext private val context: Context +) { - fun notify(items: List, student: Student) { + suspend fun notify(items: List, student: Student) { val today = LocalDateTime.now() - val lines = items.filter { !it.date.isBefore(today) }.map { - "${it.date.toFormattedString("dd.MM")} - ${it.title}: ${it.subject}" - }.ifEmpty { return } + val lines = items.filter { !it.date.isBefore(today) } + .map { + "${it.date.toFormattedString("dd.MM")} - ${it.title}: ${it.subject}" + } + .ifEmpty { return } - val notification = MultipleNotifications( - type = NotificationType.NEW_CONFERENCE, - icon = R.drawable.ic_more_conferences, - titleStringRes = R.plurals.conference_notify_new_item_title, - contentStringRes = R.plurals.conference_notify_new_items, - summaryStringRes = R.plurals.conference_number_item, - startMenu = MainView.Section.CONFERENCE, - lines = lines + val notificationDataList = lines.map { + NotificationData( + title = context.getPlural(R.plurals.conference_notify_new_item_title, 1), + content = it, + intentToStart = SplashActivity.getStartIntent(context, Destination.Conference) + ) + } + + val groupNotificationData = GroupNotificationData( + notificationDataList = notificationDataList, + title = context.getPlural(R.plurals.conference_notify_new_item_title, lines.size), + content = context.getPlural( + R.plurals.conference_notify_new_items, + lines.size, + lines.size + ), + intentToStart = SplashActivity.getStartIntent(context, Destination.Conference), + type = NotificationType.NEW_CONFERENCE ) - sendNotification(notification, student) + appNotificationManager.sendMultipleNotifications(groupNotificationData, student) } } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewExamNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewExamNotification.kt index d493c4d2..b3cf04c4 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewExamNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewExamNotification.kt @@ -1,38 +1,52 @@ package io.github.wulkanowy.services.sync.notifications import android.content.Context -import androidx.core.app.NotificationManagerCompat import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Exam import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.data.pojos.MultipleNotifications -import io.github.wulkanowy.ui.modules.main.MainView +import io.github.wulkanowy.data.pojos.GroupNotificationData +import io.github.wulkanowy.data.pojos.NotificationData +import io.github.wulkanowy.ui.modules.Destination +import io.github.wulkanowy.ui.modules.splash.SplashActivity +import io.github.wulkanowy.utils.getPlural import io.github.wulkanowy.utils.toFormattedString import java.time.LocalDate import javax.inject.Inject class NewExamNotification @Inject constructor( - @ApplicationContext private val context: Context, - notificationManager: NotificationManagerCompat, -) : BaseNotification(context, notificationManager) { + private val appNotificationManager: AppNotificationManager, + @ApplicationContext private val context: Context +) { - fun notify(items: List, student: Student) { + suspend fun notify(items: List, student: Student) { val today = LocalDate.now() - val lines = items.filter { !it.date.isBefore(today) }.map { - "${it.date.toFormattedString("dd.MM")} - ${it.subject}: ${it.description}" - }.ifEmpty { return } + val lines = items.filter { !it.date.isBefore(today) } + .map { + "${it.date.toFormattedString("dd.MM")} - ${it.subject}: ${it.description}" + } + .ifEmpty { return } - val notification = MultipleNotifications( - type = NotificationType.NEW_EXAM, - icon = R.drawable.ic_main_exam, - titleStringRes = R.plurals.exam_notify_new_item_title, - contentStringRes = R.plurals.exam_notify_new_item_content, - summaryStringRes = R.plurals.exam_number_item, - startMenu = MainView.Section.EXAM, - lines = lines + val notificationDataList = lines.map { + NotificationData( + title = context.getPlural(R.plurals.exam_notify_new_item_title, 1), + content = it, + intentToStart = SplashActivity.getStartIntent(context, Destination.Exam), + ) + } + + val groupNotificationData = GroupNotificationData( + notificationDataList = notificationDataList, + title = context.getPlural(R.plurals.exam_notify_new_item_title, lines.size), + content = context.getPlural( + R.plurals.exam_notify_new_item_content, + lines.size, + lines.size + ), + intentToStart = SplashActivity.getStartIntent(context, Destination.Exam), + type = NotificationType.NEW_EXAM ) - sendNotification(notification, student) + appNotificationManager.sendMultipleNotifications(groupNotificationData, student) } } 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 415ba343..39ecbe33 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 @@ -1,66 +1,88 @@ package io.github.wulkanowy.services.sync.notifications import android.content.Context -import androidx.core.app.NotificationManagerCompat 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.GradeSummary import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.data.pojos.MultipleNotifications -import io.github.wulkanowy.ui.modules.main.MainView +import io.github.wulkanowy.data.pojos.GroupNotificationData +import io.github.wulkanowy.data.pojos.NotificationData +import io.github.wulkanowy.ui.modules.Destination +import io.github.wulkanowy.ui.modules.splash.SplashActivity +import io.github.wulkanowy.utils.getPlural import javax.inject.Inject class NewGradeNotification @Inject constructor( - @ApplicationContext private val context: Context, - notificationManager: NotificationManagerCompat, -) : BaseNotification(context, notificationManager) { + private val appNotificationManager: AppNotificationManager, + @ApplicationContext private val context: Context +) { - fun notifyDetails(items: List, student: Student) { - val notification = MultipleNotifications( - type = NotificationType.NEW_GRADE_DETAILS, - icon = R.drawable.ic_stat_grade, - titleStringRes = R.plurals.grade_new_items, - contentStringRes = R.plurals.grade_notify_new_items, - summaryStringRes = R.plurals.grade_number_item, - startMenu = MainView.Section.GRADE, - lines = items.map { - "${it.subject}: ${it.entry}" - } + suspend fun notifyDetails(items: List, student: Student) { + val notificationDataList = items.map { + NotificationData( + title = context.getPlural(R.plurals.grade_new_items, 1), + content = "${it.subject}: ${it.entry}", + intentToStart = SplashActivity.getStartIntent(context, Destination.Grade), + ) + } + + val groupNotificationData = GroupNotificationData( + notificationDataList = notificationDataList, + title = context.getPlural(R.plurals.grade_new_items, items.size), + content = context.getPlural(R.plurals.grade_notify_new_items, items.size, items.size), + intentToStart = SplashActivity.getStartIntent(context, Destination.Grade), + type = NotificationType.NEW_GRADE_DETAILS ) - sendNotification(notification, student) + appNotificationManager.sendMultipleNotifications(groupNotificationData, student) } - fun notifyPredicted(items: List, student: Student) { - val notification = MultipleNotifications( - type = NotificationType.NEW_GRADE_PREDICTED, - icon = R.drawable.ic_stat_grade, - titleStringRes = R.plurals.grade_new_items_predicted, - contentStringRes = R.plurals.grade_notify_new_items_predicted, - summaryStringRes = R.plurals.grade_number_item, - startMenu = MainView.Section.GRADE, - lines = items.map { - "${it.subject}: ${it.predictedGrade}" - } + suspend fun notifyPredicted(items: List, student: Student) { + val notificationDataList = items.map { + NotificationData( + title = context.getPlural(R.plurals.grade_new_items_predicted, 1), + content = "${it.subject}: ${it.predictedGrade}", + intentToStart = SplashActivity.getStartIntent(context, Destination.Grade), + ) + } + + val groupNotificationData = GroupNotificationData( + notificationDataList = notificationDataList, + title = context.getPlural(R.plurals.grade_new_items_predicted, items.size), + content = context.getPlural( + R.plurals.grade_notify_new_items_predicted, + items.size, + items.size + ), + intentToStart = SplashActivity.getStartIntent(context, Destination.Grade), + type = NotificationType.NEW_GRADE_PREDICTED ) - sendNotification(notification, student) + appNotificationManager.sendMultipleNotifications(groupNotificationData, student) } - fun notifyFinal(items: List, student: Student) { - val notification = MultipleNotifications( - type = NotificationType.NEW_GRADE_FINAL, - icon = R.drawable.ic_stat_grade, - titleStringRes = R.plurals.grade_new_items_final, - contentStringRes = R.plurals.grade_notify_new_items_final, - summaryStringRes = R.plurals.grade_number_item, - startMenu = MainView.Section.GRADE, - lines = items.map { - "${it.subject}: ${it.finalGrade}" - } + suspend fun notifyFinal(items: List, student: Student) { + val notificationDataList = items.map { + NotificationData( + title = context.getPlural(R.plurals.grade_new_items_final, 1), + content = "${it.subject}: ${it.finalGrade}", + intentToStart = SplashActivity.getStartIntent(context, Destination.Grade), + ) + } + + val groupNotificationData = GroupNotificationData( + notificationDataList = notificationDataList, + title = context.getPlural(R.plurals.grade_new_items_final, items.size), + content = context.getPlural( + R.plurals.grade_notify_new_items_final, + items.size, + items.size + ), + intentToStart = SplashActivity.getStartIntent(context, Destination.Grade), + type = NotificationType.NEW_GRADE_FINAL ) - sendNotification(notification, student) + appNotificationManager.sendMultipleNotifications(groupNotificationData, student) } } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewHomeworkNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewHomeworkNotification.kt index fe973cad..ff32aa66 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewHomeworkNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewHomeworkNotification.kt @@ -1,38 +1,52 @@ package io.github.wulkanowy.services.sync.notifications import android.content.Context -import androidx.core.app.NotificationManagerCompat import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Homework import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.data.pojos.MultipleNotifications -import io.github.wulkanowy.ui.modules.main.MainView +import io.github.wulkanowy.data.pojos.GroupNotificationData +import io.github.wulkanowy.data.pojos.NotificationData +import io.github.wulkanowy.ui.modules.Destination +import io.github.wulkanowy.ui.modules.splash.SplashActivity +import io.github.wulkanowy.utils.getPlural import io.github.wulkanowy.utils.toFormattedString import java.time.LocalDate import javax.inject.Inject class NewHomeworkNotification @Inject constructor( - @ApplicationContext private val context: Context, - notificationManager: NotificationManagerCompat, -) : BaseNotification(context, notificationManager) { + private val appNotificationManager: AppNotificationManager, + @ApplicationContext private val context: Context +) { - fun notify(items: List, student: Student) { + suspend fun notify(items: List, student: Student) { val today = LocalDate.now() - val lines = items.filter { !it.date.isBefore(today) }.map { - "${it.date.toFormattedString("dd.MM")} - ${it.subject}: ${it.content}" - }.ifEmpty { return } + val lines = items.filter { !it.date.isBefore(today) } + .map { + "${it.date.toFormattedString("dd.MM")} - ${it.subject}: ${it.content}" + } + .ifEmpty { return } - val notification = MultipleNotifications( + val notificationDataList = lines.map { + NotificationData( + title = context.getPlural(R.plurals.homework_notify_new_item_title, 1), + content = it, + intentToStart = SplashActivity.getStartIntent(context, Destination.Homework), + ) + } + + val groupNotificationData = GroupNotificationData( + title = context.getPlural(R.plurals.homework_notify_new_item_title, lines.size), + content = context.getPlural( + R.plurals.homework_notify_new_item_content, + lines.size, + lines.size + ), + intentToStart = SplashActivity.getStartIntent(context, Destination.Homework), type = NotificationType.NEW_HOMEWORK, - icon = R.drawable.ic_more_homework, - titleStringRes = R.plurals.homework_notify_new_item_title, - contentStringRes = R.plurals.homework_notify_new_item_content, - summaryStringRes = R.plurals.homework_number_item, - startMenu = MainView.Section.HOMEWORK, - lines = lines + notificationDataList = notificationDataList ) - sendNotification(notification, student) + appNotificationManager.sendMultipleNotifications(groupNotificationData, student) } } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewLuckyNumberNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewLuckyNumberNotification.kt index 95156c45..5c36a06c 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewLuckyNumberNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewLuckyNumberNotification.kt @@ -1,30 +1,34 @@ package io.github.wulkanowy.services.sync.notifications import android.content.Context -import androidx.core.app.NotificationManagerCompat import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.LuckyNumber import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.data.pojos.OneNotification -import io.github.wulkanowy.ui.modules.main.MainView +import io.github.wulkanowy.data.pojos.NotificationData +import io.github.wulkanowy.ui.modules.Destination +import io.github.wulkanowy.ui.modules.splash.SplashActivity import javax.inject.Inject class NewLuckyNumberNotification @Inject constructor( - @ApplicationContext private val context: Context, - notificationManager: NotificationManagerCompat, -) : BaseNotification(context, notificationManager) { + private val appNotificationManager: AppNotificationManager, + @ApplicationContext private val context: Context +) { - fun notify(item: LuckyNumber, student: Student) { - val notification = OneNotification( - type = NotificationType.NEW_LUCKY_NUMBER, - icon = R.drawable.ic_stat_luckynumber, - titleStringRes = R.string.lucky_number_notify_new_item_title, - contentStringRes = R.string.lucky_number_notify_new_item, - startMenu = MainView.Section.LUCKY_NUMBER, - contentValues = listOf(item.luckyNumber.toString()) + suspend fun notify(item: LuckyNumber, student: Student) { + val notificationData = NotificationData( + title = context.getString(R.string.lucky_number_notify_new_item_title), + content = context.getString( + R.string.lucky_number_notify_new_item, + item.luckyNumber.toString() + ), + intentToStart = SplashActivity.getStartIntent(context, Destination.LuckyNumber) ) - sendNotification(notification, student) + appNotificationManager.sendSingleNotification( + notificationData = notificationData, + notificationType = NotificationType.NEW_LUCKY_NUMBER, + student = student + ) } } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewMessageNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewMessageNotification.kt index fc364198..b98d3466 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewMessageNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewMessageNotification.kt @@ -1,33 +1,39 @@ package io.github.wulkanowy.services.sync.notifications import android.content.Context -import androidx.core.app.NotificationManagerCompat import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.data.pojos.MultipleNotifications -import io.github.wulkanowy.ui.modules.main.MainView +import io.github.wulkanowy.data.pojos.GroupNotificationData +import io.github.wulkanowy.data.pojos.NotificationData +import io.github.wulkanowy.ui.modules.Destination +import io.github.wulkanowy.ui.modules.splash.SplashActivity +import io.github.wulkanowy.utils.getPlural import javax.inject.Inject class NewMessageNotification @Inject constructor( - @ApplicationContext private val context: Context, - notificationManager: NotificationManagerCompat, -) : BaseNotification(context, notificationManager) { + private val appNotificationManager: AppNotificationManager, + @ApplicationContext private val context: Context +) { - fun notify(items: List, student: Student) { - val notification = MultipleNotifications( - type = NotificationType.NEW_MESSAGE, - icon = R.drawable.ic_stat_message, - titleStringRes = R.plurals.message_new_items, - contentStringRes = R.plurals.message_notify_new_items, - summaryStringRes = R.plurals.message_number_item, - startMenu = MainView.Section.MESSAGE, - lines = items.map { - "${it.sender}: ${it.subject}" - } + suspend fun notify(items: List, student: Student) { + val notificationDataList = items.map { + NotificationData( + title = context.getPlural(R.plurals.message_new_items, 1), + content = "${it.sender}: ${it.subject}", + intentToStart = SplashActivity.getStartIntent(context, Destination.Message), + ) + } + + val groupNotificationData = GroupNotificationData( + notificationDataList = notificationDataList, + title = context.getPlural(R.plurals.message_new_items, items.size), + content = context.getPlural(R.plurals.message_notify_new_items, items.size, items.size), + intentToStart = SplashActivity.getStartIntent(context, Destination.Message), + type = NotificationType.NEW_MESSAGE ) - sendNotification(notification, student) + appNotificationManager.sendMultipleNotifications(groupNotificationData, student) } } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewNoteNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewNoteNotification.kt index f355341b..65520e01 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewNoteNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewNoteNotification.kt @@ -1,46 +1,46 @@ package io.github.wulkanowy.services.sync.notifications import android.content.Context -import androidx.core.app.NotificationManagerCompat import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Note import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.data.pojos.MultipleNotifications +import io.github.wulkanowy.data.pojos.GroupNotificationData +import io.github.wulkanowy.data.pojos.NotificationData import io.github.wulkanowy.sdk.scrapper.notes.NoteCategory -import io.github.wulkanowy.ui.modules.main.MainView +import io.github.wulkanowy.ui.modules.Destination +import io.github.wulkanowy.ui.modules.splash.SplashActivity +import io.github.wulkanowy.utils.getPlural import javax.inject.Inject class NewNoteNotification @Inject constructor( - @ApplicationContext private val context: Context, - notificationManager: NotificationManagerCompat, -) : BaseNotification(context, notificationManager) { + private val appNotificationManager: AppNotificationManager, + @ApplicationContext private val context: Context +) { - fun notify(items: List, student: Student) { - val notification = MultipleNotifications( - type = NotificationType.NEW_NOTE, - icon = R.drawable.ic_stat_note, - titleStringRes = when (NoteCategory.getByValue(items.first().categoryType)) { + suspend fun notify(items: List, student: Student) { + val notificationDataList = items.map { + val titleRes = when (NoteCategory.getByValue(it.categoryType)) { NoteCategory.POSITIVE -> R.plurals.praise_new_items NoteCategory.NEUTRAL -> R.plurals.neutral_note_new_items else -> R.plurals.note_new_items - }, - contentStringRes = when (NoteCategory.getByValue(items.first().categoryType)) { - NoteCategory.POSITIVE -> R.plurals.praise_notify_new_items - NoteCategory.NEUTRAL -> R.plurals.neutral_note_notify_new_items - else -> R.plurals.note_notify_new_items - }, - summaryStringRes = when (NoteCategory.getByValue(items.first().categoryType)) { - NoteCategory.POSITIVE -> R.plurals.praise_number_item - NoteCategory.NEUTRAL -> R.plurals.neutral_note_number_item - else -> R.plurals.note_number_item - }, - startMenu = MainView.Section.NOTE, - lines = items.map { - "${it.teacher}: ${it.category}" } + + NotificationData( + title = context.getPlural(titleRes, 1), + content = "${it.teacher}: ${it.category}", + intentToStart = SplashActivity.getStartIntent(context, Destination.Note), + ) + } + + val groupNotificationData = GroupNotificationData( + notificationDataList = notificationDataList, + intentToStart = SplashActivity.getStartIntent(context, Destination.Note), + title = context.getPlural(R.plurals.note_new_items, items.size), + content = context.getPlural(R.plurals.note_notify_new_items, items.size, items.size), + type = NotificationType.NEW_NOTE ) - sendNotification(notification, student) + appNotificationManager.sendMultipleNotifications(groupNotificationData, student) } } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewSchoolAnnouncementNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewSchoolAnnouncementNotification.kt index b38e4f60..6b839d29 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewSchoolAnnouncementNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewSchoolAnnouncementNotification.kt @@ -1,33 +1,54 @@ package io.github.wulkanowy.services.sync.notifications import android.content.Context -import androidx.core.app.NotificationManagerCompat import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.SchoolAnnouncement import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.data.pojos.MultipleNotifications -import io.github.wulkanowy.ui.modules.main.MainView +import io.github.wulkanowy.data.pojos.GroupNotificationData +import io.github.wulkanowy.data.pojos.NotificationData +import io.github.wulkanowy.ui.modules.Destination +import io.github.wulkanowy.ui.modules.splash.SplashActivity +import io.github.wulkanowy.utils.getPlural import javax.inject.Inject class NewSchoolAnnouncementNotification @Inject constructor( - @ApplicationContext private val context: Context, - notificationManager: NotificationManagerCompat, -) : BaseNotification(context, notificationManager) { + private val appNotificationManager: AppNotificationManager, + @ApplicationContext private val context: Context +) { - fun notify(items: List, student: Student) { - val notification = MultipleNotifications( + suspend fun notify(items: List, student: Student) { + val notificationDataList = items.map { + NotificationData( + intentToStart = SplashActivity.getStartIntent( + context = context, + destination = Destination.SchoolAnnouncement + ), + title = context.getPlural( + R.plurals.school_announcement_notify_new_item_title, + 1 + ), + content = "${it.subject}: ${it.content}" + ) + } + val groupNotificationData = GroupNotificationData( type = NotificationType.NEW_ANNOUNCEMENT, - icon = R.drawable.ic_all_about, - titleStringRes = R.plurals.school_announcement_notify_new_item_title, - contentStringRes = R.plurals.school_announcement_notify_new_items, - summaryStringRes = R.plurals.school_announcement_number_item, - startMenu = MainView.Section.SCHOOL_ANNOUNCEMENT, - lines = items.map { - "${it.subject}: ${it.content}" - } + intentToStart = SplashActivity.getStartIntent( + context = context, + destination = Destination.SchoolAnnouncement + ), + title = context.getPlural( + R.plurals.school_announcement_notify_new_item_title, + items.size + ), + content = context.getPlural( + R.plurals.school_announcement_notify_new_items, + items.size, + items.size + ), + notificationDataList = notificationDataList ) - sendNotification(notification, student) + 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 c3df1960..af79fcd2 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 @@ -1,6 +1,8 @@ package io.github.wulkanowy.services.sync.notifications +import io.github.wulkanowy.R import io.github.wulkanowy.services.sync.channels.LuckyNumberChannel +import io.github.wulkanowy.services.sync.channels.NewAttendanceChannel import io.github.wulkanowy.services.sync.channels.NewConferencesChannel import io.github.wulkanowy.services.sync.channels.NewExamChannel import io.github.wulkanowy.services.sync.channels.NewGradesChannel @@ -8,16 +10,77 @@ import io.github.wulkanowy.services.sync.channels.NewHomeworkChannel import io.github.wulkanowy.services.sync.channels.NewMessagesChannel import io.github.wulkanowy.services.sync.channels.NewNotesChannel import io.github.wulkanowy.services.sync.channels.NewSchoolAnnouncementsChannel +import io.github.wulkanowy.services.sync.channels.PushChannel +import io.github.wulkanowy.services.sync.channels.TimetableChangeChannel -enum class NotificationType(val group: String, val channel: String) { - NEW_CONFERENCE("new_conferences_group", NewConferencesChannel.CHANNEL_ID), - NEW_EXAM("new_exam_group", NewExamChannel.CHANNEL_ID), - NEW_GRADE_DETAILS("new_grade_details_group", NewGradesChannel.CHANNEL_ID), - NEW_GRADE_PREDICTED("new_grade_predicted_group", NewGradesChannel.CHANNEL_ID), - NEW_GRADE_FINAL("new_grade_final_group", NewGradesChannel.CHANNEL_ID), - NEW_HOMEWORK("new_homework_group", NewHomeworkChannel.CHANNEL_ID), - NEW_LUCKY_NUMBER("lucky_number_group", LuckyNumberChannel.CHANNEL_ID), - NEW_MESSAGE("new_message_group", NewMessagesChannel.CHANNEL_ID), - NEW_NOTE("new_notes_group", NewNotesChannel.CHANNEL_ID), - NEW_ANNOUNCEMENT("new_school_announcements_group", NewSchoolAnnouncementsChannel.CHANNEL_ID), +enum class NotificationType( + val group: String?, + val channel: String, + val icon: Int +) { + NEW_CONFERENCE( + group = "new_conferences_group", + channel = NewConferencesChannel.CHANNEL_ID, + icon = R.drawable.ic_more_conferences, + ), + NEW_EXAM( + group = "new_exam_group", + channel = NewExamChannel.CHANNEL_ID, + icon = R.drawable.ic_main_exam + ), + NEW_GRADE_DETAILS( + group = "new_grade_details_group", + channel = NewGradesChannel.CHANNEL_ID, + icon = R.drawable.ic_stat_grade, + ), + NEW_GRADE_PREDICTED( + group = "new_grade_predicted_group", + channel = NewGradesChannel.CHANNEL_ID, + icon = R.drawable.ic_stat_grade, + ), + NEW_GRADE_FINAL( + group = "new_grade_final_group", + channel = NewGradesChannel.CHANNEL_ID, + icon = R.drawable.ic_stat_grade, + ), + NEW_HOMEWORK( + group = "new_homework_group", + channel = NewHomeworkChannel.CHANNEL_ID, + icon = R.drawable.ic_more_homework, + ), + NEW_LUCKY_NUMBER( + group = null, + channel = LuckyNumberChannel.CHANNEL_ID, + icon = R.drawable.ic_stat_luckynumber, + ), + NEW_MESSAGE( + group = "new_message_group", + channel = NewMessagesChannel.CHANNEL_ID, + icon = R.drawable.ic_stat_message, + ), + NEW_NOTE( + group = "new_notes_group", + channel = NewNotesChannel.CHANNEL_ID, + icon = R.drawable.ic_stat_note + ), + NEW_ANNOUNCEMENT( + group = "new_school_announcements_group", + channel = NewSchoolAnnouncementsChannel.CHANNEL_ID, + icon = R.drawable.ic_all_about + ), + CHANGE_TIMETABLE( + group = "change_timetable_group", + channel = TimetableChangeChannel.CHANNEL_ID, + icon = R.drawable.ic_main_timetable + ), + NEW_ATTENDANCE( + group = "new_attendance_group", + channel = NewAttendanceChannel.CHANNEL_ID, + icon = R.drawable.ic_main_attendance + ), + PUSH( + group = null, + channel = PushChannel.CHANNEL_ID, + icon = R.drawable.ic_stat_all + ) } 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 788e4ea2..f7b680e3 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 @@ -3,15 +3,40 @@ package io.github.wulkanowy.services.sync.works import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.repositories.AttendanceRepository -import io.github.wulkanowy.utils.monday -import io.github.wulkanowy.utils.sunday +import io.github.wulkanowy.data.repositories.PreferencesRepository +import io.github.wulkanowy.services.sync.notifications.NewAttendanceNotification +import io.github.wulkanowy.utils.previousOrSameSchoolDay import io.github.wulkanowy.utils.waitForResult +import kotlinx.coroutines.flow.first import java.time.LocalDate.now import javax.inject.Inject -class AttendanceWork @Inject constructor(private val attendanceRepository: AttendanceRepository) : Work { +class AttendanceWork @Inject constructor( + private val attendanceRepository: AttendanceRepository, + private val newAttendanceNotification: NewAttendanceNotification, + private val preferencesRepository: PreferencesRepository +) : Work { override suspend fun doWork(student: Student, semester: Semester) { - attendanceRepository.getAttendance(student, semester, now().monday, now().sunday, true).waitForResult() + attendanceRepository.getAttendance( + student = student, + semester = semester, + start = now().previousOrSameSchoolDay, + end = now().previousOrSameSchoolDay, + forceRefresh = true, + notify = preferencesRepository.isNotificationsEnable + ) + .waitForResult() + + attendanceRepository.getAttendanceFromDatabase(semester, now().minusDays(7), now()) + .first() + .filterNot { it.isNotified } + .let { + if (it.isNotEmpty()) newAttendanceNotification.notify(it, student) + + attendanceRepository.updateTimetable(it.onEach { attendance -> + attendance.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 da2dcc7f..2a5d2d7c 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,8 +5,7 @@ import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.repositories.HomeworkRepository import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.services.sync.notifications.NewHomeworkNotification -import io.github.wulkanowy.utils.monday -import io.github.wulkanowy.utils.sunday +import io.github.wulkanowy.utils.nextOrSameSchoolDay import io.github.wulkanowy.utils.waitForResult import kotlinx.coroutines.flow.first import java.time.LocalDate.now @@ -22,13 +21,13 @@ class HomeworkWork @Inject constructor( homeworkRepository.getHomework( student = student, semester = semester, - start = now().monday, - end = now().sunday, + start = now().nextOrSameSchoolDay, + end = now().nextOrSameSchoolDay, forceRefresh = true, notify = preferencesRepository.isNotificationsEnable ).waitForResult() - homeworkRepository.getHomeworkFromDatabase(semester, now().monday, now().sunday).first() + homeworkRepository.getHomeworkFromDatabase(semester, now(), now().plusDays(7)).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 2df2c9dc..fcc33063 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 @@ -2,18 +2,41 @@ package io.github.wulkanowy.services.sync.works import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student +import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.TimetableRepository -import io.github.wulkanowy.utils.monday -import io.github.wulkanowy.utils.sunday +import io.github.wulkanowy.services.sync.notifications.ChangeTimetableNotification +import io.github.wulkanowy.utils.nextOrSameSchoolDay import io.github.wulkanowy.utils.waitForResult +import kotlinx.coroutines.flow.first import java.time.LocalDate.now import javax.inject.Inject class TimetableWork @Inject constructor( - private val timetableRepository: TimetableRepository + private val timetableRepository: TimetableRepository, + private val changeTimetableNotification: ChangeTimetableNotification, + private val preferencesRepository: PreferencesRepository ) : Work { override suspend fun doWork(student: Student, semester: Semester) { - timetableRepository.getTimetable(student, semester, now().monday, now().sunday, true).waitForResult() + timetableRepository.getTimetable( + student = student, + semester = semester, + start = now().nextOrSameSchoolDay, + end = now().nextOrSameSchoolDay, + forceRefresh = true, + notify = preferencesRepository.isNotificationsEnable + ) + .waitForResult() + + timetableRepository.getTimetableFromDatabase(semester, now(), now().plusDays(7)) + .first() + .filterNot { it.isNotified } + .let { + if (it.isNotEmpty()) changeTimetableNotification.notify(it, student) + + timetableRepository.updateTimetable(it.onEach { timetable -> + timetable.isNotified = true + }) + } } } 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 0521b4a0..075557a5 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/BaseActivity.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/BaseActivity.kt @@ -1,14 +1,11 @@ package io.github.wulkanowy.ui.base import android.app.ActivityManager -import android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK -import android.content.Intent.FLAG_ACTIVITY_NEW_TASK import android.os.Bundle import android.view.View import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity -import androidx.appcompat.app.AppCompatDelegate import androidx.viewbinding.ViewBinding import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar.LENGTH_LONG @@ -40,7 +37,6 @@ abstract class BaseActivity, VB : ViewBinding> : themeManager.applyActivityTheme(this) super.onCreate(savedInstanceState) supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleLogger, true) - AppCompatDelegate.setCompatVectorFromResourcesEnabled(true) @Suppress("DEPRECATION") setTaskDescription( @@ -83,8 +79,8 @@ abstract class BaseActivity, VB : ViewBinding> : } override fun openClearLoginView() { - startActivity(LoginActivity.getStartIntent(this) - .apply { addFlags(FLAG_ACTIVITY_CLEAR_TASK or FLAG_ACTIVITY_NEW_TASK) }) + startActivity(LoginActivity.getStartIntent(this)) + finishAffinity() } override fun onDestroy() { diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/BaseFragmentPagerAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/base/BaseFragmentPagerAdapter.kt index bd735535..6bca87f1 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/BaseFragmentPagerAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/BaseFragmentPagerAdapter.kt @@ -2,32 +2,33 @@ package io.github.wulkanowy.ui.base import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager -import androidx.fragment.app.FragmentPagerAdapter +import androidx.lifecycle.Lifecycle +import androidx.viewpager2.adapter.FragmentStateAdapter +import com.google.android.material.tabs.TabLayout +import com.google.android.material.tabs.TabLayoutMediator -//TODO Use ViewPager2 -class BaseFragmentPagerAdapter(private val fragmentManager: FragmentManager) : - FragmentPagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { +class BaseFragmentPagerAdapter( + private val fragmentManager: FragmentManager, + private val pagesCount: Int, + lifecycle: Lifecycle, +) : FragmentStateAdapter(fragmentManager, lifecycle), TabLayoutMediator.TabConfigurationStrategy { - private val pages = mutableMapOf() + lateinit var itemFactory: (position: Int) -> Fragment + + var titleFactory: (position: Int) -> String? = { "" } var containerId = 0 fun getFragmentInstance(position: Int): Fragment? { require(containerId != 0) { "Container id is 0" } - return fragmentManager.findFragmentByTag("android:switcher:$containerId:$position") + return fragmentManager.findFragmentByTag("f$position") } - fun addFragments(fragments: List) { - fragments.forEach { pages[it] = null } + override fun createFragment(position: Int): Fragment = itemFactory(position) + + override fun getItemCount() = pagesCount + + override fun onConfigureTab(tab: TabLayout.Tab, position: Int) { + tab.text = titleFactory(position) } - - fun addFragmentsWithTitle(pages: Map) { - this.pages.putAll(pages) - } - - override fun getItem(position: Int) = pages.keys.elementAt(position) - - override fun getCount() = pages.size - - override fun getPageTitle(position: Int) = pages.values.elementAt(position) } 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 6f363bfc..5cd5d010 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 @@ -6,29 +6,27 @@ import io.github.wulkanowy.utils.flowWithResource import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancelChildren import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import timber.log.Timber -import kotlin.coroutines.CoroutineContext open class BasePresenter( protected val errorHandler: ErrorHandler, protected val studentRepository: StudentRepository -) : CoroutineScope { +) { + private val job = SupervisorJob() - private var job = Job() + protected val presenterScope = CoroutineScope(job + Dispatchers.Main) - private val jobs = mutableMapOf() - - override val coroutineContext: CoroutineContext - get() = Dispatchers.Main + job + private val childrenJobs = mutableMapOf() var view: T? = null open fun onAttachView(view: T) { - job = Job() this.view = view errorHandler.apply { showErrorMessage = view::showError @@ -64,22 +62,22 @@ open class BasePresenter( } fun Flow.launch(individualJobTag: String = "load"): Job { - jobs[individualJobTag]?.cancel() - val job = catch { errorHandler.dispatch(it) }.launchIn(this@BasePresenter) - jobs[individualJobTag] = job + childrenJobs[individualJobTag]?.cancel() + val job = catch { errorHandler.dispatch(it) }.launchIn(presenterScope) + childrenJobs[individualJobTag] = job Timber.d("Job $individualJobTag launched in ${this@BasePresenter.javaClass.simpleName}: $job") return job } fun cancelJobs(vararg names: String) { names.forEach { - jobs[it]?.cancel() + childrenJobs[it]?.cancel() } } open fun onDetachView() { - view = null - job.cancel() + job.cancelChildren() errorHandler.clear() + view = null } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/ErrorDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/base/ErrorDialog.kt index 4ce97770..4c279d81 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/ErrorDialog.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/ErrorDialog.kt @@ -11,6 +11,7 @@ import android.widget.Toast import android.widget.Toast.LENGTH_LONG import androidx.appcompat.app.AlertDialog import androidx.core.content.getSystemService +import androidx.core.view.isGone import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.databinding.DialogErrorBinding @@ -24,8 +25,6 @@ import io.github.wulkanowy.utils.openEmailClient import io.github.wulkanowy.utils.openInternetBrowser import okhttp3.internal.http2.StreamResetException import java.io.InterruptedIOException -import java.io.PrintWriter -import java.io.StringWriter import java.net.ConnectException import java.net.SocketTimeoutException import java.net.UnknownHostException @@ -64,26 +63,26 @@ class ErrorDialog : BaseDialogFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - val stringWriter = StringWriter().apply { - error.printStackTrace(PrintWriter(this)) - } + val errorStacktrace = error.stackTraceToString() with(binding) { - errorDialogContent.text = stringWriter.toString() + errorDialogContent.text = errorStacktrace.replace(": ${error.localizedMessage}", "") with(errorDialogHorizontalScroll) { post { fullScroll(HorizontalScrollView.FOCUS_LEFT) } } errorDialogCopy.setOnClickListener { - val clip = ClipData.newPlainText("wulkanowy", stringWriter.toString()) + val clip = ClipData.newPlainText("Error details", errorStacktrace) activity?.getSystemService()?.setPrimaryClip(clip) Toast.makeText(context, R.string.all_copied, LENGTH_LONG).show() } errorDialogCancel.setOnClickListener { dismiss() } errorDialogReport.setOnClickListener { - openConfirmDialog { openEmailClient(stringWriter.toString()) } + openConfirmDialog { openEmailClient(errorStacktrace) } } - errorDialogMessage.text = resources.getString(error) + errorDialogHumanizedMessage.text = resources.getString(error) + errorDialogErrorMessage.text = error.localizedMessage + errorDialogErrorMessage.isGone = error.localizedMessage.isNullOrBlank() errorDialogReport.isEnabled = when (error) { is UnknownHostException, is InterruptedIOException, 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 7c32ef18..fbc994e2 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 @@ -1,6 +1,7 @@ package io.github.wulkanowy.ui.base -import android.content.res.Resources +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.data.exceptions.NoCurrentStudentException import io.github.wulkanowy.sdk.scrapper.login.BadCredentialsException import io.github.wulkanowy.sdk.scrapper.login.PasswordChangeRequiredException @@ -9,7 +10,7 @@ import io.github.wulkanowy.utils.security.ScramblerException import timber.log.Timber import javax.inject.Inject -open class ErrorHandler @Inject constructor(protected val resources: Resources) { +open class ErrorHandler @Inject constructor(@ApplicationContext protected val context: Context) { var showErrorMessage: (String, Throwable) -> Unit = { _, _ -> } @@ -25,7 +26,7 @@ open class ErrorHandler @Inject constructor(protected val resources: Resources) } protected open fun proceed(error: Throwable) { - showErrorMessage(resources.getString(error), error) + showErrorMessage(context.resources.getString(error), error) when (error) { is PasswordChangeRequiredException -> onPasswordChangeRequired(error.redirectUrl) is ScramblerException, is BadCredentialsException -> onSessionExpired() diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/ThemeManager.kt b/app/src/main/java/io/github/wulkanowy/ui/base/ThemeManager.kt index b560ed2e..e934a182 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/ThemeManager.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/ThemeManager.kt @@ -41,14 +41,15 @@ class ThemeManager @Inject constructor(private val preferencesRepository: Prefer ) } - private fun isThemeApplicable(activity: AppCompatActivity): Boolean { - return activity.packageManager + private fun isThemeApplicable(activity: AppCompatActivity) = + activity.packageManager .getPackageInfo(activity.packageName, GET_ACTIVITIES) - .activities.singleOrNull { it.name == activity::class.java.canonicalName } - ?.theme.let { + .activities + .singleOrNull { it.name == activity::class.java.canonicalName } + ?.theme + .let { it == R.style.WulkanowyTheme_Black || it == R.style.WulkanowyTheme_NoActionBar || it == R.style.WulkanowyTheme_Login || it == R.style.WulkanowyTheme_Login_Black || it == R.style.WulkanowyTheme_MessageSend || it == R.style.WulkanowyTheme_MessageSend_Black } - } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/Destination.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/Destination.kt new file mode 100644 index 00000000..43d4b5f9 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/Destination.kt @@ -0,0 +1,136 @@ +package io.github.wulkanowy.ui.modules + +import androidx.fragment.app.Fragment +import io.github.wulkanowy.ui.modules.attendance.AttendanceFragment +import io.github.wulkanowy.ui.modules.conference.ConferenceFragment +import io.github.wulkanowy.ui.modules.dashboard.DashboardFragment +import io.github.wulkanowy.ui.modules.exam.ExamFragment +import io.github.wulkanowy.ui.modules.grade.GradeFragment +import io.github.wulkanowy.ui.modules.homework.HomeworkFragment +import io.github.wulkanowy.ui.modules.luckynumber.LuckyNumberFragment +import io.github.wulkanowy.ui.modules.message.MessageFragment +import io.github.wulkanowy.ui.modules.more.MoreFragment +import io.github.wulkanowy.ui.modules.note.NoteFragment +import io.github.wulkanowy.ui.modules.schoolandteachers.school.SchoolFragment +import io.github.wulkanowy.ui.modules.schoolannouncement.SchoolAnnouncementFragment +import io.github.wulkanowy.ui.modules.timetable.TimetableFragment +import java.io.Serializable +import java.time.LocalDate + +sealed interface Destination : Serializable { + + /* + Type in children classes have to be as getter to avoid null in enums + https://stackoverflow.com/questions/68866453/kotlin-enum-val-is-returning-null-despite-being-set-at-compile-time + */ + val type: Type + + val fragment: Fragment + + enum class Type(val defaultDestination: Destination) { + DASHBOARD(Dashboard), + GRADE(Grade), + ATTENDANCE(Attendance), + EXAM(Exam), + TIMETABLE(Timetable()), + HOMEWORK(Homework), + NOTE(Note), + CONFERENCE(Conference), + SCHOOL_ANNOUNCEMENT(SchoolAnnouncement), + SCHOOL(School), + LUCKY_NUMBER(More), + MORE(More), + MESSAGE(Message); + } + + object Dashboard : Destination { + + override val type get() = Type.DASHBOARD + + override val fragment get() = DashboardFragment.newInstance() + } + + object Grade : Destination { + + override val type get() = Type.GRADE + + override val fragment get() = GradeFragment.newInstance() + } + + object Attendance : Destination { + + override val type get() = Type.ATTENDANCE + + override val fragment get() = AttendanceFragment.newInstance() + } + + object Exam : Destination { + + override val type get() = Type.EXAM + + override val fragment get() = ExamFragment.newInstance() + } + + data class Timetable(val date: LocalDate? = null) : Destination { + + override val type get() = Type.TIMETABLE + + override val fragment get() = TimetableFragment.newInstance(date) + } + + object Homework : Destination { + + override val type get() = Type.HOMEWORK + + override val fragment get() = HomeworkFragment.newInstance() + } + + object Note : Destination { + + override val type get() = Type.NOTE + + override val fragment get() = NoteFragment.newInstance() + } + + object Conference : Destination { + + override val type get() = Type.CONFERENCE + + override val fragment get() = ConferenceFragment.newInstance() + } + + object SchoolAnnouncement : Destination { + + override val type get() = Type.SCHOOL_ANNOUNCEMENT + + override val fragment get() = SchoolAnnouncementFragment.newInstance() + } + + object School : Destination { + + override val type get() = Type.SCHOOL + + override val fragment get() = SchoolFragment.newInstance() + } + + object LuckyNumber : Destination { + + override val type get() = Type.LUCKY_NUMBER + + override val fragment get() = LuckyNumberFragment.newInstance() + } + + object More : Destination { + + override val type get() = Type.MORE + + override val fragment get() = MoreFragment.newInstance() + } + + object Message : Destination { + + override val type get() = Type.MESSAGE + + override val fragment get() = MessageFragment.newInstance() + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/about/AboutPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/about/AboutPresenter.kt index 6bcf5f77..55274934 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/about/AboutPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/about/AboutPresenter.kt @@ -82,18 +82,20 @@ class AboutPresenter @Inject constructor( private fun loadData() { view?.run { - updateData(listOfNotNull( - versionRes, - creatorsRes, - feedbackRes, - faqRes, - discordRes, - facebookRes, - twitterRes, - homepageRes, - licensesRes, - privacyRes - )) + updateData( + listOfNotNull( + versionRes, + creatorsRes, + feedbackRes, + faqRes, + discordRes, + facebookRes, + twitterRes, + homepageRes, + licensesRes, + privacyRes + ) + ) } } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/about/license/LicensePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/about/license/LicensePresenter.kt index cc430fc2..5368cc19 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/about/license/LicensePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/about/license/LicensePresenter.kt @@ -31,7 +31,7 @@ class LicensePresenter @Inject constructor( private fun loadData() { flowWithResource { - withContext(dispatchers.backgroundThread) { + withContext(dispatchers.io) { view?.appLibraries.orEmpty() } }.onEach { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountdetails/AccountDetailsFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountdetails/AccountDetailsFragment.kt index a9890ad9..c3137ec5 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountdetails/AccountDetailsFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountdetails/AccountDetailsFragment.kt @@ -121,10 +121,14 @@ class AccountDetailsFragment : } } - override fun popView() { + override fun popViewToMain() { (requireActivity() as MainActivity).popView(2) } + override fun popViewToAccounts() { + (requireActivity() as MainActivity).popView(1) + } + override fun recreateMainView() { requireActivity().recreate() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountdetails/AccountDetailsPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountdetails/AccountDetailsPresenter.kt index d4cba580..1f44cbbc 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountdetails/AccountDetailsPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountdetails/AccountDetailsPresenter.kt @@ -119,7 +119,7 @@ class AccountDetailsPresenter @Inject constructor( } } }.afterLoading { - view?.popView() + view?.popViewToMain() }.launch("switch") } @@ -152,11 +152,14 @@ class AccountDetailsPresenter @Inject constructor( syncManager.stopSyncWorker() openClearLoginView() } - studentWithSemesters!!.student.isCurrent -> { + studentWithSemesters?.student?.isCurrent == true -> { Timber.i("Logout result: Logout student and switch to another") recreateMainView() } - else -> Timber.i("Logout result: Logout student") + else -> { + Timber.i("Logout result: Logout student") + recreateMainView() + } } } Status.ERROR -> { @@ -165,7 +168,11 @@ class AccountDetailsPresenter @Inject constructor( } } }.afterLoading { - view?.popView() + if (studentWithSemesters?.student?.isCurrent == true) { + view?.popViewToMain() + } else { + view?.popViewToAccounts() + } }.launch("logout") } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountdetails/AccountDetailsView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountdetails/AccountDetailsView.kt index 652f0c1a..aeb743fa 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountdetails/AccountDetailsView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountdetails/AccountDetailsView.kt @@ -15,7 +15,9 @@ interface AccountDetailsView : BaseView { fun showLogoutConfirmDialog() - fun popView() + fun popViewToMain() + + fun popViewToAccounts() fun recreateMainView() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountedit/AccountEditColorAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountedit/AccountEditColorAdapter.kt index ab6eec41..66e39fc7 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountedit/AccountEditColorAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountedit/AccountEditColorAdapter.kt @@ -3,12 +3,9 @@ package io.github.wulkanowy.ui.modules.account.accountedit import android.annotation.SuppressLint import android.content.res.ColorStateList import android.graphics.Color -import android.graphics.drawable.ColorDrawable import android.graphics.drawable.Drawable import android.graphics.drawable.GradientDrawable import android.graphics.drawable.RippleDrawable -import android.graphics.drawable.StateListDrawable -import android.os.Build import android.view.LayoutInflater import android.view.ViewGroup import androidx.core.view.isVisible @@ -52,30 +49,13 @@ class AccountEditColorAdapter @Inject constructor() : } } - private fun Int.createForegroundDrawable(): Drawable = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - val mask = GradientDrawable().apply { - shape = GradientDrawable.OVAL - setColor(Color.BLACK) - } - RippleDrawable(ColorStateList.valueOf(this.rippleColor), null, mask) - } else { - val foreground = StateListDrawable().apply { - alpha = 80 - setEnterFadeDuration(250) - setExitFadeDuration(250) - } - - val mask = GradientDrawable().apply { - shape = GradientDrawable.OVAL - setColor(this@createForegroundDrawable.rippleColor) - } - - foreground.apply { - addState(intArrayOf(android.R.attr.state_pressed), mask) - addState(intArrayOf(), ColorDrawable(Color.TRANSPARENT)) - } + private fun Int.createForegroundDrawable(): Drawable { + val mask = GradientDrawable().apply { + shape = GradientDrawable.OVAL + setColor(Color.BLACK) } + return RippleDrawable(ColorStateList.valueOf(this.rippleColor), null, mask) + } private inline val Int.rippleColor: Int get() { 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 6cee2396..5d5ed504 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 @@ -9,7 +9,7 @@ import io.github.wulkanowy.R 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.description +import io.github.wulkanowy.utils.descriptionRes import io.github.wulkanowy.utils.isExcusableOrNotExcused import javax.inject.Inject @@ -36,7 +36,7 @@ class AttendanceAdapter @Inject constructor() : with(holder.binding) { attendanceItemNumber.text = item.number.toString() attendanceItemSubject.text = item.subject - attendanceItemDescription.setText(item.description) + attendanceItemDescription.setText(item.descriptionRes) attendanceItemAlert.visibility = item.run { if (absence && !excused) View.VISIBLE else View.INVISIBLE } attendanceItemNumber.visibility = View.GONE attendanceItemExcuseInfo.visibility = View.GONE @@ -46,7 +46,7 @@ class AttendanceAdapter @Inject constructor() : onExcuseCheckboxSelect(item, checked) } - when (if (item.excuseStatus != null) SentExcuseStatus.valueOf(item.excuseStatus) else null) { + when (item.excuseStatus?.let { SentExcuseStatus.valueOf(it)}) { SentExcuseStatus.WAITING -> { attendanceItemExcuseInfo.setImageResource(R.drawable.ic_excuse_waiting) attendanceItemExcuseInfo.visibility = View.VISIBLE 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 d816d8f0..9b5c63e4 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 @@ -7,7 +7,7 @@ import android.view.ViewGroup import androidx.fragment.app.DialogFragment import io.github.wulkanowy.data.db.entities.Attendance import io.github.wulkanowy.databinding.DialogAttendanceBinding -import io.github.wulkanowy.utils.description +import io.github.wulkanowy.utils.descriptionRes import io.github.wulkanowy.utils.lifecycleAwareVariable import io.github.wulkanowy.utils.toFormattedString @@ -45,7 +45,7 @@ class AttendanceDialog : DialogFragment() { with(binding) { attendanceDialogSubjectValue.text = attendance.subject - attendanceDialogDescriptionValue.setText(attendance.description) + attendanceDialogDescriptionValue.setText(attendance.descriptionRes) 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/AttendanceFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceFragment.kt index 05894679..bd9a6692 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceFragment.kt @@ -12,6 +12,7 @@ import android.view.View.INVISIBLE import android.view.View.VISIBLE import androidx.appcompat.app.AlertDialog import androidx.appcompat.view.ActionMode +import androidx.core.view.isVisible import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.datepicker.CalendarConstraints import com.google.android.material.datepicker.MaterialDatePicker @@ -121,9 +122,7 @@ class AttendanceFragment : BaseFragment(R.layout.frag attendanceSwipe.setOnRefreshListener(presenter::onSwipeRefresh) attendanceSwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary)) attendanceSwipe.setProgressBackgroundColorSchemeColor( - requireContext().getThemeAttrColor( - R.attr.colorSwipeRefresh - ) + requireContext().getThemeAttrColor(R.attr.colorSwipeRefresh) ) attendanceErrorRetry.setOnClickListener { presenter.onRetry() } attendanceErrorDetails.setOnClickListener { presenter.onDetailsClick() } @@ -134,7 +133,7 @@ class AttendanceFragment : BaseFragment(R.layout.frag attendanceExcuseButton.setOnClickListener { presenter.onExcuseButtonClick() } - attendanceNavContainer.setElevationCompat(requireContext().dpToPx(8f)) + attendanceNavContainer.elevation = requireContext().dpToPx(8f) } } @@ -218,7 +217,7 @@ class AttendanceFragment : BaseFragment(R.layout.frag } override fun showExcuseButton(show: Boolean) { - binding.attendanceExcuseButton.visibility = if (show) VISIBLE else GONE + binding.attendanceExcuseButton.isVisible = show } override fun showAttendanceDialog(lesson: Attendance) { @@ -245,7 +244,9 @@ class AttendanceFragment : BaseFragment(R.layout.frag presenter.onDateSet(date.year, date.monthValue, date.dayOfMonth) } - datePicker.show(this@AttendanceFragment.parentFragmentManager, null) + if (!parentFragmentManager.isStateSaved) { + datePicker.show(parentFragmentManager, null) + } } override fun showExcuseDialog() { @@ -287,12 +288,16 @@ class AttendanceFragment : BaseFragment(R.layout.frag } override fun showExcuseCheckboxes(show: Boolean) { - attendanceAdapter.apply { + with(attendanceAdapter) { excuseActionMode = show notifyDataSetChanged() } } + override fun showDayNavigation(show: Boolean) { + binding.attendanceNavContainer.isVisible = show + } + override fun finishActionMode() { actionMode?.finish() } 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 9a159812..54d29bcf 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 @@ -152,6 +152,8 @@ class AttendancePresenter @Inject constructor( fun onExcuseDialogSubmit(reason: String) { view?.finishActionMode() + if (attendanceToExcuseList.isEmpty()) return + if (isVulcanExcusedFunctionEnabled) { excuseAbsence( reason = reason.takeIf { it.isNotBlank() }, @@ -172,6 +174,8 @@ class AttendancePresenter @Inject constructor( view?.apply { showExcuseCheckboxes(true) showExcuseButton(false) + enableSwipe(false) + showDayNavigation(false) } attendanceToExcuseList.clear() return true @@ -181,6 +185,8 @@ class AttendancePresenter @Inject constructor( view?.apply { showExcuseCheckboxes(false) showExcuseButton(true) + enableSwipe(true) + showDayNavigation(true) } } @@ -234,6 +240,7 @@ class AttendancePresenter @Inject constructor( enableSwipe(true) showRefresh(true) showProgress(false) + showErrorView(false) showEmpty(filteredAttendance.isEmpty()) showContent(filteredAttendance.isNotEmpty()) updateData(filteredAttendance.sortedBy { item -> item.number }) @@ -256,9 +263,8 @@ class AttendancePresenter @Inject constructor( showEmpty(filteredAttendance.isEmpty()) showErrorView(false) showContent(filteredAttendance.isNotEmpty()) - showExcuseButton(filteredAttendance.any { item -> - (!isParent && isVulcanExcusedFunctionEnabled) || (isParent && item.isExcusableOrNotExcused) - }) + val anyExcusables = filteredAttendance.any { it.isExcusableOrNotExcused } + showExcuseButton(anyExcusables && (isParent || isVulcanExcusedFunctionEnabled)) } analytics.logEvent( "load_data", diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceView.kt index 738f2ff8..7ddd75f4 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceView.kt @@ -60,6 +60,8 @@ interface AttendanceView : BaseView { fun showExcuseCheckboxes(show: Boolean) + fun showDayNavigation(show: Boolean) + fun finishActionMode() fun popView() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/summary/AttendanceSummaryFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/summary/AttendanceSummaryFragment.kt index 118971e6..dd1644a9 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/summary/AttendanceSummaryFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/summary/AttendanceSummaryFragment.kt @@ -71,7 +71,7 @@ class AttendanceSummaryFragment : setOnItemSelectedListener { presenter.onSubjectSelected(it?.text?.toString()) } } - binding.attendanceSummarySubjectsContainer.setElevationCompat(requireContext().dpToPx(1f)) + binding.attendanceSummarySubjectsContainer.elevation = requireContext().dpToPx(1f) } override fun updateSubjects(data: ArrayList) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/summary/AttendanceSummaryPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/summary/AttendanceSummaryPresenter.kt index e53cda74..8b603837 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/summary/AttendanceSummaryPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/summary/AttendanceSummaryPresenter.kt @@ -82,7 +82,13 @@ class AttendanceSummaryPresenter @Inject constructor( flowWithResourceIn { val student = studentRepository.getCurrentStudent() val semester = semesterRepository.getCurrentSemester(student) - attendanceSummaryRepository.getAttendanceSummary(student, semester, subjectId, forceRefresh) + + attendanceSummaryRepository.getAttendanceSummary( + student = student, + semester = semester, + subjectId = subjectId, + forceRefresh = forceRefresh + ) }.onEach { when (it.status) { Status.LOADING -> { @@ -92,6 +98,7 @@ class AttendanceSummaryPresenter @Inject constructor( showRefresh(true) showProgress(false) showContent(true) + showErrorView(false) updateDataSet(sortItems(it.data)) } } @@ -99,6 +106,7 @@ class AttendanceSummaryPresenter @Inject constructor( Status.SUCCESS -> { Timber.i("Loading attendance summary result: Success") view?.apply { + showErrorView(false) showEmpty(it.data!!.isEmpty()) showContent(it.data.isNotEmpty()) updateDataSet(sortItems(it.data)) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferenceAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferenceAdapter.kt index c8728614..f63b293c 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferenceAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferenceAdapter.kt @@ -14,6 +14,8 @@ class ConferenceAdapter @Inject constructor() : var items = emptyList() + var onItemClickListener: (Conference) -> Unit = {} + override fun getItemCount() = items.size override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ItemViewHolder( @@ -28,7 +30,10 @@ class ConferenceAdapter @Inject constructor() : conferenceItemTitle.text = item.title conferenceItemSubject.text = item.subject conferenceItemContent.text = item.agenda - conferenceItemContent.visibility = if (item.agenda.isBlank()) View.GONE else View.VISIBLE + conferenceItemContent.visibility = + if (item.agenda.isBlank()) View.GONE else View.VISIBLE + + root.setOnClickListener { onItemClickListener(item) } } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferenceDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferenceDialog.kt new file mode 100644 index 00000000..477b762b --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferenceDialog.kt @@ -0,0 +1,60 @@ +package io.github.wulkanowy.ui.modules.conference + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.view.isVisible +import androidx.fragment.app.DialogFragment +import io.github.wulkanowy.data.db.entities.Conference +import io.github.wulkanowy.databinding.DialogConferenceBinding +import io.github.wulkanowy.utils.lifecycleAwareVariable +import io.github.wulkanowy.utils.toFormattedString + +class ConferenceDialog : DialogFragment() { + + private var binding: DialogConferenceBinding by lifecycleAwareVariable() + + private lateinit var conference: Conference + + companion object { + + private const val ARGUMENT_KEY = "item" + + fun newInstance(conference: Conference) = ConferenceDialog().apply { + arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, conference) } + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setStyle(STYLE_NO_TITLE, 0) + arguments?.let { + conference = it.getSerializable(ARGUMENT_KEY) as Conference + } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ) = DialogConferenceBinding.inflate(inflater).also { binding = it }.root + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + with(binding) { + conferenceDialogClose.setOnClickListener { dismiss() } + + conferenceDialogSubjectValue.text = conference.subject + conferenceDialogDateValue.text = conference.date.toFormattedString("dd.MM.yyyy HH:mm") + conferenceDialogHeaderValue.text = conference.title + conferenceDialogAgendaValue.text = conference.agenda + conferenceDialogPresentValue.text = conference.presentOnConference + conferenceDialogPresentValue.isVisible = conference.presentOnConference.isNotBlank() + conferenceDialogPresentTitle.isVisible = conference.presentOnConference.isNotBlank() + conferenceDialogAgendaValue.isVisible = conference.agenda.isNotBlank() + conferenceDialogAgendaTitle.isVisible = conference.agenda.isNotBlank() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferenceFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferenceFragment.kt index dd10a65e..b9642b1c 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferenceFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferenceFragment.kt @@ -8,6 +8,7 @@ import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Conference import io.github.wulkanowy.databinding.FragmentConferenceBinding import io.github.wulkanowy.ui.base.BaseFragment +import io.github.wulkanowy.ui.modules.main.MainActivity import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.widgets.DividerItemDecoration import io.github.wulkanowy.utils.getThemeAttrColor @@ -41,6 +42,8 @@ class ConferenceFragment : BaseFragment(R.layout.frag } override fun initView() { + conferencesAdapter.onItemClickListener = presenter::onItemSelected + with(binding.conferenceRecycler) { layoutManager = LinearLayoutManager(context) adapter = conferencesAdapter @@ -50,7 +53,11 @@ class ConferenceFragment : BaseFragment(R.layout.frag with(binding) { conferenceSwipe.setOnRefreshListener(presenter::onSwipeRefresh) conferenceSwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary)) - conferenceSwipe.setProgressBackgroundColorSchemeColor(requireContext().getThemeAttrColor(R.attr.colorSwipeRefresh)) + conferenceSwipe.setProgressBackgroundColorSchemeColor( + requireContext().getThemeAttrColor( + R.attr.colorSwipeRefresh + ) + ) conferenceErrorRetry.setOnClickListener { presenter.onRetry() } conferenceErrorDetails.setOnClickListener { presenter.onDetailsClick() } } @@ -98,6 +105,10 @@ class ConferenceFragment : BaseFragment(R.layout.frag binding.conferenceRecycler.visibility = if (show) View.VISIBLE else View.GONE } + override fun openConferenceDialog(conference: Conference) { + (activity as? MainActivity)?.showDialogFragment(ConferenceDialog.newInstance(conference)) + } + override fun onDestroyView() { presenter.onDetachView() super.onDestroyView() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferencePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferencePresenter.kt index cc7e50db..dab170da 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferencePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferencePresenter.kt @@ -1,6 +1,7 @@ package io.github.wulkanowy.ui.modules.conference import io.github.wulkanowy.data.Status +import io.github.wulkanowy.data.db.entities.Conference import io.github.wulkanowy.data.repositories.ConferenceRepository import io.github.wulkanowy.data.repositories.SemesterRepository import io.github.wulkanowy.data.repositories.StudentRepository @@ -43,6 +44,10 @@ class ConferencePresenter @Inject constructor( loadData(true) } + fun onItemSelected(conference: Conference) { + view?.openConferenceDialog(conference) + } + fun onDetailsClick() { view?.showErrorDetailsDialog(lastError) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferenceView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferenceView.kt index f3d1b3b3..4f73394d 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferenceView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferenceView.kt @@ -26,4 +26,6 @@ interface ConferenceView : BaseView { fun enableSwipe(enable: Boolean) fun showContent(show: Boolean) + + fun openConferenceDialog(conference: Conference) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAdapter.kt index 56503d02..440bbd5d 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAdapter.kt @@ -1,6 +1,8 @@ package io.github.wulkanowy.ui.modules.dashboard import android.annotation.SuppressLint +import android.content.res.ColorStateList +import android.graphics.Color import android.graphics.Typeface import android.os.Handler import android.os.Looper @@ -18,6 +20,7 @@ import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Timetable import io.github.wulkanowy.data.db.entities.TimetableHeader import io.github.wulkanowy.databinding.ItemDashboardAccountBinding +import io.github.wulkanowy.databinding.ItemDashboardAdminMessageBinding import io.github.wulkanowy.databinding.ItemDashboardAnnouncementsBinding import io.github.wulkanowy.databinding.ItemDashboardConferencesBinding import io.github.wulkanowy.databinding.ItemDashboardExamsBinding @@ -31,6 +34,7 @@ import io.github.wulkanowy.utils.getThemeAttrColor import io.github.wulkanowy.utils.left import io.github.wulkanowy.utils.nickOrName import io.github.wulkanowy.utils.toFormattedString +import timber.log.Timber import java.time.Duration import java.time.LocalDate import java.time.LocalDateTime @@ -52,7 +56,7 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter Unit = {} - var onLessonsTileClickListener: () -> Unit = {} + var onLessonsTileClickListener: (LocalDate) -> Unit = {} var onHomeworkTileClickListener: () -> Unit = {} @@ -62,6 +66,8 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter Unit = {} + var onAdminMessageClickListener: (String?) -> Unit = {} + val items = mutableListOf() fun submitList(newItems: List) { @@ -108,6 +114,9 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter ConferencesViewHolder( ItemDashboardConferencesBinding.inflate(inflater, parent, false) ) + DashboardItem.Type.ADMIN_MESSAGE.ordinal -> AdminMessageViewHolder( + ItemDashboardAdminMessageBinding.inflate(inflater, parent, false) + ) else -> throw IllegalArgumentException() } } @@ -122,6 +131,7 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter bindAnnouncementsViewHolder(holder, position) is ExamsViewHolder -> bindExamsViewHolder(holder, position) is ConferencesViewHolder -> bindConferencesViewHolder(holder, position) + is AdminMessageViewHolder -> bindAdminMessage(holder, position) } } @@ -170,6 +180,8 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter { context.getThemeAttrColor(R.attr.colorOnSurface) @@ -199,13 +211,12 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter { @@ -220,7 +231,8 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter { matchConstraintPercentWidth = when { luckyNumber == null && unreadMessagesCount == null -> 1.0f @@ -232,7 +244,8 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter { + dateToNavigate = currentDate updateLessonView(item, currentTimetable, binding) binding.dashboardLessonsItemTitleTomorrow.isVisible = false + binding.dashboardLessonsItemTitleTodayAndTomorrow.isVisible = false } tomorrowTimetable.isNotEmpty() -> { + dateToNavigate = tomorrowDate updateLessonView(item, tomorrowTimetable, binding) binding.dashboardLessonsItemTitleTomorrow.isVisible = true + binding.dashboardLessonsItemTitleTodayAndTomorrow.isVisible = false } currentDayHeader != null && currentDayHeader.content.isNotBlank() -> { + dateToNavigate = currentDate updateLessonView(item, emptyList(), binding, currentDayHeader) binding.dashboardLessonsItemTitleTomorrow.isVisible = false + binding.dashboardLessonsItemTitleTodayAndTomorrow.isVisible = false } tomorrowDayHeader != null && tomorrowDayHeader.content.isNotBlank() -> { + dateToNavigate = tomorrowDate updateLessonView(item, emptyList(), binding, tomorrowDayHeader) binding.dashboardLessonsItemTitleTomorrow.isVisible = true + binding.dashboardLessonsItemTitleTodayAndTomorrow.isVisible = false } else -> { + dateToNavigate = currentDate updateLessonView(item, emptyList(), binding) - binding.dashboardLessonsItemTitleTomorrow.isVisible = + binding.dashboardLessonsItemTitleTomorrow.isVisible = false + binding.dashboardLessonsItemTitleTodayAndTomorrow.isVisible = !(item.isLoading && item.error == null) } } @@ -322,7 +348,7 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter { + context.getThemeAttrColor(R.attr.colorPrimary) to + context.getThemeAttrColor(R.attr.colorOnPrimary) + } + "MEDIUM" -> { + context.getThemeAttrColor(R.attr.colorMessageMedium) to Color.BLACK + } + else -> null to context.getThemeAttrColor(R.attr.colorOnSurface) + } + + with(adminMessageViewHolder.binding) { + dashboardAdminMessageItemTitle.text = item.title + dashboardAdminMessageItemTitle.setTextColor(textColor) + dashboardAdminMessageItemDescription.text = item.content + dashboardAdminMessageItemDescription.setTextColor(textColor) + dashboardAdminMessageItemIcon.setColorFilter(textColor) + + root.setCardBackgroundColor(backgroundColor?.let { ColorStateList.valueOf(it) }) + item.destinationUrl?.let { url -> + root.setOnClickListener { onAdminMessageClickListener(url) } + } + } + } + class AccountViewHolder(val binding: ItemDashboardAccountBinding) : RecyclerView.ViewHolder(binding.root) @@ -717,6 +774,9 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter, private val oldList: List 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 1bd258b2..775b7b55 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 @@ -10,6 +10,7 @@ import androidx.core.view.isVisible import androidx.recyclerview.widget.DefaultItemAnimator import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.databinding.FragmentDashboardBinding @@ -24,10 +25,12 @@ import io.github.wulkanowy.ui.modules.luckynumber.LuckyNumberFragment import io.github.wulkanowy.ui.modules.main.MainActivity import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.modules.message.MessageFragment +import io.github.wulkanowy.ui.modules.notificationscenter.NotificationsCenterFragment import io.github.wulkanowy.ui.modules.schoolannouncement.SchoolAnnouncementFragment import io.github.wulkanowy.ui.modules.timetable.TimetableFragment import io.github.wulkanowy.utils.capitalise 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 @@ -70,10 +73,7 @@ class DashboardFragment : BaseFragment(R.layout.fragme override fun initView() { val mainActivity = requireActivity() as MainActivity val itemTouchHelper = ItemTouchHelper( - DashboardItemMoveCallback( - dashboardAdapter, - presenter::onDragAndDropEnd - ) + DashboardItemMoveCallback(dashboardAdapter, presenter::onDragAndDropEnd) ) dashboardAdapter.apply { @@ -87,7 +87,9 @@ class DashboardFragment : BaseFragment(R.layout.fragme onAttendanceTileClickListener = { mainActivity.pushView(AttendanceSummaryFragment.newInstance()) } - onLessonsTileClickListener = { mainActivity.pushView(TimetableFragment.newInstance()) } + onLessonsTileClickListener = { + mainActivity.pushView(TimetableFragment.newInstance(it)) + } onGradeTileClickListener = { mainActivity.pushView(GradeFragment.newInstance()) } onHomeworkTileClickListener = { mainActivity.pushView(HomeworkFragment.newInstance()) } onAnnouncementsTileClickListener = { @@ -97,6 +99,13 @@ class DashboardFragment : BaseFragment(R.layout.fragme onConferencesTileClickListener = { mainActivity.pushView(ConferenceFragment.newInstance()) } + onAdminMessageClickListener = presenter::onAdminMessageSelected + + registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { + override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { + binding.dashboardRecycler.scrollToPosition(0) + } + }) } with(binding) { @@ -121,6 +130,7 @@ class DashboardFragment : BaseFragment(R.layout.fragme override fun onOptionsItemSelected(item: MenuItem): Boolean { return when (item.itemId) { R.id.dashboard_menu_tiles -> presenter.onDashboardTileSettingsSelected() + R.id.dashboard_menu_notifaction_list -> presenter.onNotificationsCenterSelected() else -> false } } @@ -183,6 +193,14 @@ class DashboardFragment : BaseFragment(R.layout.fragme if (::presenter.isInitialized) presenter.onViewReselected() } + override fun openNotificationsCenterView() { + (requireActivity() as MainActivity).pushView(NotificationsCenterFragment.newInstance()) + } + + override fun openInternetBrowser(url: String) { + requireContext().openInternetBrowser(url) + } + override fun onDestroyView() { dashboardAdapter.clearTimers() presenter.onDetachView() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardItem.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardItem.kt index cf99f0c9..92665857 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardItem.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardItem.kt @@ -1,5 +1,6 @@ package io.github.wulkanowy.ui.modules.dashboard +import io.github.wulkanowy.data.db.entities.AdminMessage import io.github.wulkanowy.data.db.entities.Conference import io.github.wulkanowy.data.db.entities.Exam import io.github.wulkanowy.data.db.entities.Grade @@ -16,6 +17,15 @@ sealed class DashboardItem(val type: Type) { abstract val isDataLoaded: Boolean + data class AdminMessages( + val adminMessage: AdminMessage? = null, + override val error: Throwable? = null, + override val isLoading: Boolean = false + ) : DashboardItem(Type.ADMIN_MESSAGE) { + + override val isDataLoaded get() = adminMessage != null + } + data class Account( val student: Student? = null, override val error: Throwable? = null, @@ -96,6 +106,7 @@ sealed class DashboardItem(val type: Type) { } enum class Type { + ADMIN_MESSAGE, ACCOUNT, HORIZONTAL_GROUP, LESSONS, @@ -108,6 +119,7 @@ sealed class DashboardItem(val type: Type) { } enum class Tile { + ADMIN_MESSAGE, ACCOUNT, LUCKY_NUMBER, MESSAGES, @@ -123,6 +135,7 @@ sealed class DashboardItem(val type: Type) { } fun DashboardItem.Tile.toDashboardItemType() = when (this) { + DashboardItem.Tile.ADMIN_MESSAGE -> DashboardItem.Type.ADMIN_MESSAGE DashboardItem.Tile.ACCOUNT -> DashboardItem.Type.ACCOUNT DashboardItem.Tile.LUCKY_NUMBER -> DashboardItem.Type.HORIZONTAL_GROUP DashboardItem.Tile.MESSAGES -> DashboardItem.Type.HORIZONTAL_GROUP diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardItemMoveCallback.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardItemMoveCallback.kt index cf4097a4..b9625570 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardItemMoveCallback.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardItemMoveCallback.kt @@ -21,7 +21,7 @@ class DashboardItemMoveCallback( recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder ): Int { - val dragFlags = if (viewHolder.bindingAdapterPosition != 0) { + val dragFlags = if (!viewHolder.isAdminMessageOrAccountItem) { ItemTouchHelper.UP or ItemTouchHelper.DOWN } else 0 @@ -32,7 +32,7 @@ class DashboardItemMoveCallback( recyclerView: RecyclerView, current: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder - ) = target.bindingAdapterPosition != 0 + ) = !target.isAdminMessageOrAccountItem override fun onMove( recyclerView: RecyclerView, @@ -52,4 +52,7 @@ class DashboardItemMoveCallback( onUserInteractionEndListener(dashboardAdapter.items.toList()) } + + private val RecyclerView.ViewHolder.isAdminMessageOrAccountItem: Boolean + get() = this is DashboardAdapter.AdminMessageViewHolder || this is DashboardAdapter.AccountViewHolder } 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 027bcc05..2ec198be 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 @@ -5,6 +5,7 @@ import io.github.wulkanowy.data.Status import io.github.wulkanowy.data.db.entities.LuckyNumber import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.enums.MessageFolder +import io.github.wulkanowy.data.repositories.AdminMessageRepository import io.github.wulkanowy.data.repositories.AttendanceSummaryRepository import io.github.wulkanowy.data.repositories.ConferenceRepository import io.github.wulkanowy.data.repositories.ExamRepository @@ -50,7 +51,8 @@ class DashboardPresenter @Inject constructor( private val examRepository: ExamRepository, private val conferenceRepository: ConferenceRepository, private val preferencesRepository: PreferencesRepository, - private val schoolAnnouncementRepository: SchoolAnnouncementRepository + private val schoolAnnouncementRepository: SchoolAnnouncementRepository, + private val adminMessageRepository: AdminMessageRepository ) : BasePresenter(errorHandler, studentRepository) { private val dashboardItemLoadedList = mutableListOf() @@ -149,7 +151,7 @@ class DashboardPresenter @Inject constructor( tileList: List, forceRefresh: Boolean ) { - launch { + presenterScope.launch { Timber.i("Loading dashboard account data started") val student = runCatching { studentRepository.getCurrentStudent(true) } .onFailure { @@ -179,6 +181,7 @@ class DashboardPresenter @Inject constructor( loadConferences(student, forceRefresh) } DashboardItem.Type.ADS -> TODO() + DashboardItem.Type.ADMIN_MESSAGE -> loadAdminMessage(student, forceRefresh) } } } @@ -209,6 +212,11 @@ class DashboardPresenter @Inject constructor( view?.showErrorDetailsDialog(lastError) } + fun onNotificationsCenterSelected(): Boolean { + view?.openNotificationsCenterView() + return true + } + fun onDashboardTileSettingsSelected(): Boolean { view?.showDashboardTileSettings(preferencesRepository.selectedDashboardTiles.toList()) return true @@ -220,6 +228,10 @@ class DashboardPresenter @Inject constructor( }.toSet() } + fun onAdminMessageSelected(url: String?) { + url?.let { view?.openInternetBrowser(it) } + } + private fun loadHorizontalGroup(student: Student, forceRefresh: Boolean) { flow { val semester = semesterRepository.getCurrentSemester(student) @@ -304,18 +316,17 @@ class DashboardPresenter @Inject constructor( gradeRepository.getGrades(student, semester, forceRefresh) }.map { originalResource -> - val filteredSubjectWithGrades = originalResource.data?.first.orEmpty() - .filter { grade -> - grade.date.isAfter(LocalDate.now().minusDays(7)) - } - .groupBy { grade -> grade.subject } + val filteredSubjectWithGrades = originalResource.data?.first + .orEmpty() + .filter { it.date >= LocalDate.now().minusDays(7) } + .groupBy { it.subject } .mapValues { entry -> entry.value .take(5) - .sortedBy { grade -> grade.date } + .sortedByDescending { it.date } } .toList() - .sortedBy { subjectWithGrades -> subjectWithGrades.second[0].date } + .sortedByDescending { (_, grades) -> grades[0].date } .toMap() Resource( @@ -419,9 +430,9 @@ class DashboardPresenter @Inject constructor( }.map { homeworkResource -> val currentDate = LocalDate.now() - val filteredHomework = homeworkResource.data?.filter { - (it.date.isAfter(currentDate) || it.date == currentDate) && !it.isDone - } + val filteredHomework = homeworkResource.data + ?.filter { (it.date.isAfter(currentDate) || it.date == currentDate) && !it.isDone } + ?.sortedBy { it.date } homeworkResource.copy(data = filteredHomework) }.onEach { @@ -492,31 +503,37 @@ class DashboardPresenter @Inject constructor( end = LocalDate.now().plusDays(7), forceRefresh = forceRefresh ) - }.onEach { - when (it.status) { - Status.LOADING -> { - Timber.i("Loading dashboard exams data started") - if (forceRefresh) return@onEach - updateData( - DashboardItem.Exams(it.data.orEmpty(), isLoading = true), - forceRefresh - ) + } + .map { examResource -> + val sortedExams = examResource.data?.sortedBy { it.date } - if (!it.data.isNullOrEmpty()) { - firstLoadedItemList += DashboardItem.Type.EXAMS + examResource.copy(data = sortedExams) + } + .onEach { + when (it.status) { + Status.LOADING -> { + Timber.i("Loading dashboard exams data started") + if (forceRefresh) return@onEach + updateData( + DashboardItem.Exams(it.data.orEmpty(), isLoading = true), + forceRefresh + ) + + if (!it.data.isNullOrEmpty()) { + firstLoadedItemList += DashboardItem.Type.EXAMS + } + } + Status.SUCCESS -> { + Timber.i("Loading dashboard exams result: Success") + updateData(DashboardItem.Exams(it.data ?: emptyList()), forceRefresh) + } + Status.ERROR -> { + Timber.i("Loading dashboard exams result: An exception occurred") + errorHandler.dispatch(it.error!!) + updateData(DashboardItem.Exams(error = it.error), forceRefresh) } } - Status.SUCCESS -> { - Timber.i("Loading dashboard exams result: Success") - updateData(DashboardItem.Exams(it.data ?: emptyList()), forceRefresh) - } - Status.ERROR -> { - Timber.i("Loading dashboard exams result: An exception occurred") - errorHandler.dispatch(it.error!!) - updateData(DashboardItem.Exams(error = it.error), forceRefresh) - } - } - }.launch("dashboard_exams") + }.launch("dashboard_exams") } private fun loadConferences(student: Student, forceRefresh: Boolean) { @@ -556,6 +573,38 @@ class DashboardPresenter @Inject constructor( }.launch("dashboard_conferences") } + private fun loadAdminMessage(student: Student, forceRefresh: Boolean) { + flowWithResourceIn { adminMessageRepository.getAdminMessages(student, forceRefresh) } + .onEach { + when (it.status) { + Status.LOADING -> { + Timber.i("Loading dashboard admin message data started") + if (forceRefresh) return@onEach + updateData(DashboardItem.AdminMessages(), forceRefresh) + } + Status.SUCCESS -> { + Timber.i("Loading dashboard admin message result: Success") + updateData( + dashboardItem = DashboardItem.AdminMessages(adminMessage = it.data), + forceRefresh = forceRefresh + ) + } + Status.ERROR -> { + Timber.i("Loading dashboard admin message result: An exception occurred") + errorHandler.dispatch(it.error!!) + updateData( + dashboardItem = DashboardItem.AdminMessages( + adminMessage = it.data, + error = it.error + ), + forceRefresh = forceRefresh + ) + } + } + } + .launch("dashboard_admin_messages") + } + private fun updateData(dashboardItem: DashboardItem, forceRefresh: Boolean) { val isForceRefreshError = forceRefresh && dashboardItem.error != null val isFirstRunDataLoadedError = @@ -568,6 +617,13 @@ class DashboardPresenter @Inject constructor( sortDashboardItems() + if (dashboardItem is DashboardItem.AdminMessages && !dashboardItem.isDataLoaded) { + dashboardItemsToLoad = dashboardItemsToLoad - DashboardItem.Type.ADMIN_MESSAGE + dashboardTileLoadedList = dashboardTileLoadedList - DashboardItem.Tile.ADMIN_MESSAGE + + dashboardItemLoadedList.removeAll { it.type == DashboardItem.Type.ADMIN_MESSAGE } + } + if (forceRefresh) { updateForceRefreshData(dashboardItem) } else { @@ -599,9 +655,12 @@ class DashboardPresenter @Inject constructor( } private fun updateForceRefreshData(dashboardItem: DashboardItem) { + val isNotLoadedAdminMessage = + dashboardItem is DashboardItem.AdminMessages && !dashboardItem.isDataLoaded + with(dashboardItemRefreshLoadedList) { removeAll { it.type == dashboardItem.type } - add(dashboardItem) + if (!isNotLoadedAdminMessage) add(dashboardItem) } val isRefreshItemLoaded = @@ -633,7 +692,9 @@ class DashboardPresenter @Inject constructor( itemsLoadedList: List, forceRefresh: Boolean ) { - val filteredItems = itemsLoadedList.filterNot { it.type == DashboardItem.Type.ACCOUNT } + val filteredItems = itemsLoadedList.filterNot { + it.type == DashboardItem.Type.ACCOUNT || it.type == DashboardItem.Type.ADMIN_MESSAGE + } val isAccountItemError = itemsLoadedList.find { it.type == DashboardItem.Type.ACCOUNT }?.error != null val isGeneralError = @@ -665,10 +726,13 @@ class DashboardPresenter @Inject constructor( val dashboardItemsPosition = preferencesRepository.dashboardItemsPosition dashboardItemLoadedList.sortBy { tile -> - dashboardItemsPosition?.getOrDefault( - tile.type, + val defaultPosition = if (tile is DashboardItem.AdminMessages) { + -1 + } else { tile.type.ordinal + 100 - ) ?: tile.type.ordinal + } + + dashboardItemsPosition?.getOrDefault(tile.type, defaultPosition) ?: tile.type.ordinal } } } \ No newline at end of file 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 d5c5e5a7..730e19a3 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 @@ -23,4 +23,8 @@ interface DashboardView : BaseView { fun resetView() fun popViewToRoot() + + fun openNotificationsCenterView() + + fun openInternetBrowser(url: String) } \ No newline at end of file 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 07468daa..d0dfcd69 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 @@ -3,6 +3,8 @@ package io.github.wulkanowy.ui.modules.debug.notification import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.repositories.StudentRepository +import io.github.wulkanowy.services.sync.notifications.ChangeTimetableNotification +import io.github.wulkanowy.services.sync.notifications.NewAttendanceNotification import io.github.wulkanowy.services.sync.notifications.NewConferenceNotification import io.github.wulkanowy.services.sync.notifications.NewExamNotification import io.github.wulkanowy.services.sync.notifications.NewGradeNotification @@ -13,6 +15,7 @@ import io.github.wulkanowy.services.sync.notifications.NewNoteNotification import io.github.wulkanowy.services.sync.notifications.NewSchoolAnnouncementNotification import io.github.wulkanowy.ui.base.BasePresenter 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.debugGradeDetailsItems @@ -22,6 +25,7 @@ import io.github.wulkanowy.ui.modules.debug.notification.mock.debugLuckyNumber import io.github.wulkanowy.ui.modules.debug.notification.mock.debugMessageItems import io.github.wulkanowy.ui.modules.debug.notification.mock.debugNoteItems import io.github.wulkanowy.ui.modules.debug.notification.mock.debugSchoolAnnouncementItems +import io.github.wulkanowy.ui.modules.debug.notification.mock.debugTimetableItems import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject @@ -37,6 +41,8 @@ class NotificationDebugPresenter @Inject constructor( private val newNoteNotification: NewNoteNotification, private val newSchoolAnnouncementNotification: NewSchoolAnnouncementNotification, private val newLuckyNumberNotification: NewLuckyNumberNotification, + private val changeTimetableNotification: ChangeTimetableNotification, + private val newAttendanceNotification: NewAttendanceNotification, ) : BasePresenter(errorHandler, studentRepository) { private val items = listOf( @@ -64,6 +70,12 @@ class NotificationDebugPresenter @Inject constructor( NotificationDebugItem(R.string.note_title) { n -> withStudent { newNoteNotification.notify(debugNoteItems.take(n), it) } }, + NotificationDebugItem(R.string.attendance_title) { n -> + withStudent { newAttendanceNotification.notify(debugAttendanceItems.take(n), it) } + }, + NotificationDebugItem(R.string.timetable_title) { n -> + withStudent { changeTimetableNotification.notify(debugTimetableItems.take(n), it) } + }, NotificationDebugItem(R.string.school_announcement_title) { n -> withStudent { newSchoolAnnouncementNotification.notify(debugSchoolAnnouncementItems.take(n), it) @@ -87,8 +99,8 @@ class NotificationDebugPresenter @Inject constructor( } } - private fun withStudent(block: (Student) -> Unit) { - launch { + private fun withStudent(block: suspend (Student) -> Unit) { + presenterScope.launch { block(studentRepository.getCurrentStudent(false)) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/mock/attendance.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/mock/attendance.kt new file mode 100644 index 00000000..042cf07e --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/mock/attendance.kt @@ -0,0 +1,35 @@ +package io.github.wulkanowy.ui.modules.debug.notification.mock + +import io.github.wulkanowy.data.db.entities.Attendance +import java.time.LocalDate + +val debugAttendanceItems = listOf( + generateAttendance("Matematyka", "PRESENCE"), + generateAttendance("Język angielski", "UNEXCUSED_LATENESS"), + generateAttendance("Geografia", "ABSENCE_UNEXCUSED"), + generateAttendance("Sieci komputerowe", "ABSENCE_EXCUSED"), + generateAttendance("Systemy operacyjne", "EXCUSED_LATENESS"), + generateAttendance("Język niemiecki", "ABSENCE_UNEXCUSED"), + generateAttendance("Biologia", "ABSENCE_UNEXCUSED"), + generateAttendance("Chemia", "ABSENCE_EXCUSED"), + generateAttendance("Fizyka", "ABSENCE_UNEXCUSED"), + generateAttendance("Matematyka", "ABSENCE_EXCUSED"), +) + +private fun generateAttendance(subject: String, name: String) = Attendance( + subject = subject, + studentId = 0, + diaryId = 0, + date = LocalDate.now(), + timeId = 0, + number = 1, + name = name, + presence = false, + absence = false, + exemption = false, + lateness = false, + excused = false, + deleted = false, + excusable = false, + excuseStatus = "" +) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/mock/timetable.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/mock/timetable.kt new file mode 100644 index 00000000..428c001d --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/mock/timetable.kt @@ -0,0 +1,39 @@ +package io.github.wulkanowy.ui.modules.debug.notification.mock + +import io.github.wulkanowy.data.db.entities.Timetable +import java.time.LocalDate +import java.time.LocalDateTime +import kotlin.random.Random + +val debugTimetableItems = listOf( + generateTimetable("Matematyka", "12", "01"), + generateTimetable("Język angielski", "23", "12"), + generateTimetable("Geografia", "34", "23"), + generateTimetable("Sieci komputerowe", "45", "34"), + generateTimetable("Systemy operacyjne", "56", "45"), + generateTimetable("Język niemiecki", "67", "56"), + generateTimetable("Biologia", "78", "67"), + generateTimetable("Chemia", "89", "78"), + generateTimetable("Fizyka", "90", "89"), + generateTimetable("Matematyka", "01", "90"), +) + +private fun generateTimetable(subject: String, room: String, roomOld: String) = Timetable( + subject = subject, + studentId = 0, + diaryId = 0, + date = LocalDate.now().minusDays(Random.nextLong(0, 8)), + number = 1, + start = LocalDateTime.now().plusHours(1), + end = LocalDateTime.now(), + subjectOld = "", + group = "", + room = room, + roomOld = roomOld, + teacher = "Wtorkowska Renata", + teacherOld = "", + info = "", + isStudentPlan = true, + changes = true, + canceled = true +) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamFragment.kt index fb7939bc..ddd0e4a1 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamFragment.kt @@ -64,7 +64,7 @@ class ExamFragment : BaseFragment(R.layout.fragment_exam), examPreviousButton.setOnClickListener { presenter.onPreviousWeek() } examNextButton.setOnClickListener { presenter.onNextWeek() } - examNavContainer.setElevationCompat(requireContext().dpToPx(8f)) + examNavContainer.elevation = requireContext().dpToPx(8f) } } 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 4a304972..2784f429 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 @@ -131,7 +131,9 @@ class GradeAverageProvider @Inject constructor( val updatedFirstSemesterGrades = firstSemesterSubject?.grades?.updateModifiers(student).orEmpty() - (updatedSecondSemesterGrades + updatedFirstSemesterGrades).calcAverage(isOptionalArithmeticAverage) + (updatedSecondSemesterGrades + updatedFirstSemesterGrades).calcAverage( + isOptionalArithmeticAverage + ) } else { secondSemesterSubject.average } @@ -147,7 +149,8 @@ class GradeAverageProvider @Inject constructor( return if (!isAnyVulcanAverage || isGradeAverageForceCalc) { val secondSemesterAverage = - secondSemesterSubject.grades.updateModifiers(student).calcAverage(isOptionalArithmeticAverage) + secondSemesterSubject.grades.updateModifiers(student) + .calcAverage(isOptionalArithmeticAverage) val firstSemesterAverage = firstSemesterSubject?.grades?.updateModifiers(student) ?.calcAverage(isOptionalArithmeticAverage) ?: secondSemesterAverage @@ -213,7 +216,8 @@ class GradeAverageProvider @Inject constructor( proposedPoints = "", finalPoints = "", pointsSum = "", - average = if (calcAverage) details.updateModifiers(student).calcAverage(isOptionalArithmeticAverage) else .0 + average = if (calcAverage) details.updateModifiers(student) + .calcAverage(isOptionalArithmeticAverage) else .0 ) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeExpandMode.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeExpandMode.kt new file mode 100644 index 00000000..722e986e --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeExpandMode.kt @@ -0,0 +1,9 @@ +package io.github.wulkanowy.ui.modules.grade + +enum class GradeExpandMode(val value: String) { + ONE("one"), UNLIMITED("any"), ALWAYS_EXPANDED("always"); + + companion object { + fun getByValue(value: String) = values().firstOrNull { it.value == value } ?: ONE + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeFragment.kt index b3ef3037..0a8561ee 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeFragment.kt @@ -8,6 +8,7 @@ import android.view.View import android.view.View.INVISIBLE import android.view.View.VISIBLE import androidx.appcompat.app.AlertDialog +import com.google.android.material.tabs.TabLayoutMediator import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Semester @@ -29,7 +30,13 @@ class GradeFragment : BaseFragment(R.layout.fragment_grade @Inject lateinit var presenter: GradePresenter - private val pagerAdapter by lazy { BaseFragmentPagerAdapter(childFragmentManager) } + private val pagerAdapter by lazy { + BaseFragmentPagerAdapter( + fragmentManager = childFragmentManager, + pagesCount = 3, + lifecycle = lifecycle, + ) + } private var semesterSwitchMenu: MenuItem? = null @@ -62,28 +69,35 @@ class GradeFragment : BaseFragment(R.layout.fragment_grade } override fun initView() { - with(pagerAdapter) { - containerId = binding.gradeViewPager.id - addFragmentsWithTitle( - mapOf( - GradeDetailsFragment.newInstance() to getString(R.string.all_details), - GradeSummaryFragment.newInstance() to getString(R.string.grade_menu_summary), - GradeStatisticsFragment.newInstance() to getString(R.string.grade_menu_statistics) - ) - ) - } - with(binding.gradeViewPager) { adapter = pagerAdapter offscreenPageLimit = 3 setOnSelectPageListener(presenter::onPageSelected) } - with(binding.gradeTabLayout) { - setupWithViewPager(binding.gradeViewPager) - setElevationCompat(context.dpToPx(4f)) + with(pagerAdapter) { + containerId = binding.gradeViewPager.id + titleFactory = { + when (it) { + 0 -> getString(R.string.all_details) + 1 -> getString(R.string.grade_menu_summary) + 2 -> getString(R.string.grade_menu_statistics) + else -> throw IllegalStateException() + } + } + itemFactory = { + when (it) { + 0 -> GradeDetailsFragment.newInstance() + 1 -> GradeSummaryFragment.newInstance() + 2 -> GradeStatisticsFragment.newInstance() + else -> throw IllegalStateException() + } + } + TabLayoutMediator(binding.gradeTabLayout, binding.gradeViewPager, this).attach() } + binding.gradeTabLayout.elevation = requireContext().dpToPx(4f) + with(binding) { gradeErrorRetry.setOnClickListener { presenter.onRetry() } gradeErrorDetails.setOnClickListener { presenter.onDetailsClick() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradePresenter.kt index 504c730d..76e88bcd 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradePresenter.kt @@ -101,7 +101,6 @@ class GradePresenter @Inject constructor( private fun loadData() { flowWithResource { val student = studentRepository.getCurrentStudent() - delay(200) semesterRepository.getSemesters(student, refreshOnNoCurrent = true) }.onEach { when (it.status) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsAdapter.kt index 01631140..d96ac092 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsAdapter.kt @@ -5,6 +5,7 @@ import android.content.res.Resources import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.core.view.isVisible import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.NO_POSITION @@ -13,9 +14,11 @@ import io.github.wulkanowy.data.db.entities.Grade import io.github.wulkanowy.databinding.HeaderGradeDetailsBinding import io.github.wulkanowy.databinding.ItemGradeDetailsBinding import io.github.wulkanowy.ui.base.BaseExpandableAdapter +import io.github.wulkanowy.ui.modules.grade.GradeExpandMode import io.github.wulkanowy.utils.getBackgroundColor import io.github.wulkanowy.utils.toFormattedString import timber.log.Timber +import java.util.BitSet import javax.inject.Inject class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter() { @@ -24,19 +27,20 @@ class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter() - private var expandedPosition = NO_POSITION + private val expandedPositions = BitSet(items.size) - private var isExpandable = false + private var expandMode = GradeExpandMode.ONE var onClickListener: (Grade, position: Int) -> Unit = { _, _ -> } var colorTheme = "" - fun setDataItems(data: List, isExpanded: Boolean = isExpandable) { + fun setDataItems(data: List, expandMode: GradeExpandMode = this.expandMode) { headers = data.filter { it.viewType == ViewType.HEADER }.toMutableList() - items = if (isExpanded) headers else data.toMutableList() - isExpandable = isExpanded - expandedPosition = NO_POSITION + items = + (if (expandMode != GradeExpandMode.ALWAYS_EXPANDED) headers else data).toMutableList() + this.expandMode = expandMode + expandedPositions.clear() } fun updateDetailsItem(position: Int, grade: Grade) { @@ -48,7 +52,7 @@ class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter 1) { - Timber.e("Header with subject $subject found ${candidates.size} times! Expanded: $expandedPosition. Items: $candidates") + Timber.e("Header with subject $subject found ${candidates.size} times! Expanded: $expandedPositions. Items: $candidates") } return candidates.first() @@ -64,9 +68,9 @@ class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter HeaderViewHolder(HeaderGradeDetailsBinding.inflate(inflater, parent, false)) - ViewType.ITEM.id -> ItemViewHolder(ItemGradeDetailsBinding.inflate(inflater, parent, false)) + ViewType.HEADER.id -> HeaderViewHolder( + HeaderGradeDetailsBinding.inflate(inflater, parent, false) + ) + ViewType.ITEM.id -> ItemViewHolder( + ItemGradeDetailsBinding.inflate(inflater, parent, false) + ) else -> throw IllegalStateException() } } @@ -106,46 +114,91 @@ class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter 0) View.VISIBLE else View.GONE - if (header.newGrades > 0) gradeHeaderNote.text = header.newGrades.toString(10) + gradeHeaderPointsSum.text = + context.getString(R.string.grade_points_sum, header.pointsSum) + gradeHeaderPointsSum.isVisible = !header.pointsSum.isNullOrEmpty() + gradeHeaderNumber.text = context.resources.getQuantityString( + R.plurals.grade_number_item, + header.grades.size, + header.grades.size + ) + gradeHeaderNote.isVisible = header.newGrades > 0 - gradeHeaderContainer.isEnabled = isExpandable + if (header.newGrades > 0) { + gradeHeaderNote.text = header.newGrades.toString() + } + + gradeHeaderContainer.isEnabled = expandMode != GradeExpandMode.ALWAYS_EXPANDED gradeHeaderContainer.setOnClickListener { - expandedPosition = if (expandedPosition == adapterPosition) -1 else adapterPosition - - if (expandedPosition != NO_POSITION) { - refreshList(headers.toMutableList().apply { - addAll(headerPosition + 1, header.grades) - }) - scrollToHeaderWithSubItems(headerPosition, header.grades.size) - } else { - refreshList(headers) - } + expandGradeHeader(headerPosition, header, holder) } } } - private fun formatAverage(average: Double?, resources: Resources): String { - return if (average == null || average == .0) resources.getString(R.string.grade_no_average) - else resources.getString(R.string.grade_average, average) + private fun expandGradeHeader( + headerPosition: Int, + header: GradeDetailsHeader, + holder: HeaderViewHolder + ) { + if (expandMode == GradeExpandMode.ONE) { + val isHeaderExpanded = expandedPositions[headerPosition] + + expandedPositions.clear() + + if (!isHeaderExpanded) { + val updatedItemList = headers.toMutableList() + .apply { addAll(headerPosition + 1, header.grades) } + + expandedPositions.set(headerPosition) + refreshList(updatedItemList) + scrollToHeaderWithSubItems(headerPosition, header.grades.size) + } else { + refreshList(headers.toMutableList()) + } + } else if (expandMode == GradeExpandMode.UNLIMITED) { + val headerAdapterPosition = holder.bindingAdapterPosition + val isHeaderExpanded = expandedPositions[headerPosition] + + expandedPositions.flip(headerPosition) + + if (!isHeaderExpanded) { + val updatedList = items.toMutableList() + .apply { addAll(headerAdapterPosition + 1, header.grades) } + + refreshList(updatedList) + scrollToHeaderWithSubItems(headerAdapterPosition, header.grades.size) + } else { + val startPosition = headerAdapterPosition + 1 + val updatedList = items.toMutableList() + .apply { + subList(startPosition, startPosition + header.grades.size).clear() + } + + refreshList(updatedList) + } + } } @SuppressLint("SetTextI18n") private fun bindItemViewHolder(holder: ItemViewHolder, grade: Grade) { + val context = holder.binding.root.context + with(holder.binding) { gradeItemValue.run { text = grade.entry @@ -154,26 +207,37 @@ class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter grade.description grade.gradeSymbol.isNotBlank() -> grade.gradeSymbol - else -> root.context.getString(R.string.all_no_description) + else -> context.getString(R.string.all_no_description) } gradeItemDate.text = grade.date.toFormattedString() - gradeItemWeight.text = "${root.context.getString(R.string.grade_weight)}: ${grade.weight}" + gradeItemWeight.text = "${context.getString(R.string.grade_weight)}: ${grade.weight}" gradeItemNote.visibility = if (!grade.isRead) View.VISIBLE else View.GONE root.setOnClickListener { - holder.bindingAdapterPosition.let { if (it != NO_POSITION) onClickListener(grade, it) } + holder.bindingAdapterPosition.let { + if (it != NO_POSITION) onClickListener(grade, it) + } } } } + private fun formatAverage(average: Double?, resources: Resources) = + if (average == null || average == .0) { + resources.getString(R.string.grade_no_average) + } else { + resources.getString(R.string.grade_average, average) + } + private class HeaderViewHolder(val binding: HeaderGradeDetailsBinding) : RecyclerView.ViewHolder(binding.root) private class ItemViewHolder(val binding: ItemGradeDetailsBinding) : RecyclerView.ViewHolder(binding.root) - class GradeDetailsDiffUtil(private val old: List, private val new: List) : - DiffUtil.Callback() { + private class GradeDetailsDiffUtil( + private val old: List, + private val new: List + ) : DiffUtil.Callback() { override fun getOldListSize() = old.size diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsFragment.kt index 9d4da767..c93600d4 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsFragment.kt @@ -12,6 +12,7 @@ import androidx.recyclerview.widget.LinearLayoutManager import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Grade +import io.github.wulkanowy.ui.modules.grade.GradeExpandMode import io.github.wulkanowy.databinding.FragmentGradeDetailsBinding import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.modules.grade.GradeFragment @@ -79,10 +80,10 @@ class GradeDetailsFragment : else false } - override fun updateData(data: List, isGradeExpandable: Boolean, gradeColorTheme: String) { + override fun updateData(data: List, expandMode: GradeExpandMode, gradeColorTheme: String) { with(gradeDetailsAdapter) { colorTheme = gradeColorTheme - setDataItems(data, isGradeExpandable) + setDataItems(data, expandMode) notifyDataSetChanged() } } 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 7544d2aa..54d4f461 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 @@ -9,6 +9,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.ui.modules.grade.GradeAverageProvider +import io.github.wulkanowy.ui.modules.grade.GradeExpandMode import io.github.wulkanowy.ui.modules.grade.GradeSortingMode.ALPHABETIC import io.github.wulkanowy.ui.modules.grade.GradeSortingMode.DATE import io.github.wulkanowy.ui.modules.grade.GradeSubject @@ -16,6 +17,7 @@ import io.github.wulkanowy.utils.AnalyticsHelper import io.github.wulkanowy.utils.afterLoading import io.github.wulkanowy.utils.flowWithResource import io.github.wulkanowy.utils.flowWithResourceIn +import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.onEach import timber.log.Timber @@ -46,8 +48,8 @@ class GradeDetailsPresenter @Inject constructor( fun onParentViewLoadData(semesterId: Int, forceRefresh: Boolean) { currentSemesterId = semesterId - loadData(semesterId, forceRefresh) if (!forceRefresh) view?.showErrorView(false) + loadData(semesterId, forceRefresh) } fun onGradeItemSelected(grade: Grade, position: Int) { @@ -113,7 +115,7 @@ class GradeDetailsPresenter @Inject constructor( fun onParentViewReselected() { view?.run { if (!isViewEmpty) { - if (preferencesRepository.isGradeExpandable) collapseAllItems() + if (preferencesRepository.gradeExpandMode != GradeExpandMode.ALWAYS_EXPANDED) collapseAllItems() scrollToStart() } } @@ -157,7 +159,7 @@ class GradeDetailsPresenter @Inject constructor( showContent(true) updateData( data = items, - isGradeExpandable = preferencesRepository.isGradeExpandable, + expandMode = preferencesRepository.gradeExpandMode, gradeColorTheme = preferencesRepository.gradeColorTheme ) notifyParentDataLoaded(semesterId) @@ -175,7 +177,7 @@ class GradeDetailsPresenter @Inject constructor( showContent(items.isNotEmpty()) updateData( data = items, - isGradeExpandable = preferencesRepository.isGradeExpandable, + expandMode = preferencesRepository.gradeExpandMode, gradeColorTheme = preferencesRepository.gradeColorTheme ) } @@ -197,6 +199,9 @@ class GradeDetailsPresenter @Inject constructor( enableSwipe(true) notifyParentDataLoaded(semesterId) } + }.catch { + errorHandler.dispatch(it) + view?.notifyParentDataLoaded(semesterId) }.launch() } @@ -213,6 +218,7 @@ class GradeDetailsPresenter @Inject constructor( setErrorDetails(message) showErrorView(true) showEmpty(false) + showProgress(false) } else showError(message, error) } } @@ -235,14 +241,24 @@ class GradeDetailsPresenter @Inject constructor( .sortedByDescending { it.date } .map { GradeDetailsItem(it, ViewType.ITEM) } - listOf(GradeDetailsItem(GradeDetailsHeader( - subject = subject, - average = average, - pointsSum = points, - grades = subItems - ).apply { - newGrades = grades.filter { grade -> !grade.isRead }.size - }, ViewType.HEADER)) + if (preferencesRepository.isGradeExpandable) emptyList() else subItems + val gradeDetailsItems = listOf( + GradeDetailsItem( + GradeDetailsHeader( + subject = subject, + average = average, + pointsSum = points, + grades = subItems + ).apply { + newGrades = grades.filter { grade -> !grade.isRead }.size + }, ViewType.HEADER + ) + ) + + if (preferencesRepository.gradeExpandMode == GradeExpandMode.ALWAYS_EXPANDED) { + gradeDetailsItems + subItems + } else { + gradeDetailsItems + } }.flatten() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsView.kt index e71fcc3c..55633229 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsView.kt @@ -1,6 +1,7 @@ package io.github.wulkanowy.ui.modules.grade.details import io.github.wulkanowy.data.db.entities.Grade +import io.github.wulkanowy.ui.modules.grade.GradeExpandMode import io.github.wulkanowy.ui.base.BaseView interface GradeDetailsView : BaseView { @@ -9,7 +10,7 @@ interface GradeDetailsView : BaseView { fun initView() - fun updateData(data: List, isGradeExpandable: Boolean, gradeColorTheme: String) + fun updateData(data: List, expandMode: GradeExpandMode, gradeColorTheme: String) fun updateItem(item: Grade, position: Int) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/statistics/GradeStatisticsFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/statistics/GradeStatisticsFragment.kt index 0adac300..dbc4c10c 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/statistics/GradeStatisticsFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/statistics/GradeStatisticsFragment.kt @@ -68,7 +68,7 @@ class GradeStatisticsFragment : } with(binding) { - gradeStatisticsSubjectsContainer.setElevationCompat(requireContext().dpToPx(1f)) + gradeStatisticsSubjectsContainer.elevation = requireContext().dpToPx(1f) gradeStatisticsSwipe.setOnRefreshListener(presenter::onSwipeRefresh) gradeStatisticsSwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary)) 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 0754361c..082c847e 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 @@ -10,7 +10,7 @@ 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.utils.calcAverage +import io.github.wulkanowy.utils.calcFinalAverage import java.util.Locale import javax.inject.Inject @@ -25,6 +25,10 @@ class GradeSummaryAdapter @Inject constructor( var items = emptyList() + var onCalculatedHelpClickListener: () -> Unit = {} + + var onFinalHelpClickListener: () -> Unit = {} + override fun getItemCount() = items.size + if (items.isNotEmpty()) 1 else 0 override fun getItemViewType(position: Int) = when (position) { @@ -60,7 +64,7 @@ class GradeSummaryAdapter @Inject constructor( val finalItemsCount = items.count { it.finalGrade.matches("[0-6][+-]?".toRegex()) } val calculatedItemsCount = items.count { value -> value.average != 0.0 } val allItemsCount = items.count { !it.subject.equals("zachowanie", true) } - val finalAverage = items.calcAverage( + val finalAverage = items.calcFinalAverage( preferencesRepository.gradePlusModifier, preferencesRepository.gradeMinusModifier ) @@ -83,6 +87,9 @@ class GradeSummaryAdapter @Inject constructor( calculatedItemsCount, allItemsCount ) + + gradeSummaryCalculatedAverageHelp.setOnClickListener { onCalculatedHelpClickListener() } + gradeSummaryFinalAverageHelp.setOnClickListener { onFinalHelpClickListener() } } } 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 0ac16fb3..3810902f 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,6 +5,7 @@ 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 dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R @@ -48,6 +49,11 @@ class GradeSummaryFragment : } override fun initView() { + with(gradeSummaryAdapter) { + onCalculatedHelpClickListener = presenter::onCalculatedAverageHelpClick + onFinalHelpClickListener = presenter::onFinalAverageHelpClick + } + with(binding.gradeSummaryRecycler) { layoutManager = LinearLayoutManager(context) adapter = gradeSummaryAdapter @@ -55,7 +61,11 @@ class GradeSummaryFragment : with(binding) { gradeSummarySwipe.setOnRefreshListener(presenter::onSwipeRefresh) gradeSummarySwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary)) - gradeSummarySwipe.setProgressBackgroundColorSchemeColor(requireContext().getThemeAttrColor(R.attr.colorSwipeRefresh)) + gradeSummarySwipe.setProgressBackgroundColorSchemeColor( + requireContext().getThemeAttrColor( + R.attr.colorSwipeRefresh + ) + ) gradeSummaryErrorRetry.setOnClickListener { presenter.onRetry() } gradeSummaryErrorDetails.setOnClickListener { presenter.onDetailsClick() } } @@ -107,6 +117,22 @@ class GradeSummaryFragment : binding.gradeSummarySwipe.isRefreshing = show } + override fun showCalculatedAverageHelpDialog() { + AlertDialog.Builder(requireContext()) + .setTitle(R.string.grade_summary_calculated_average_help_dialog_title) + .setMessage(R.string.grade_summary_calculated_average_help_dialog_message) + .setPositiveButton(R.string.all_close) { _, _ -> } + .show() + } + + override fun showFinalAverageHelpDialog() { + AlertDialog.Builder(requireContext()) + .setTitle(R.string.grade_summary_final_average_help_dialog_title) + .setMessage(R.string.grade_summary_final_average_help_dialog_message) + .setPositiveButton(R.string.all_close) { _, _ -> } + .show() + } + override fun onParentLoadData(semesterId: Int, forceRefresh: Boolean) { presenter.onParentViewLoadData(semesterId, forceRefresh) } 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 7adfd7e5..933633dc 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 @@ -135,6 +135,14 @@ class GradeSummaryPresenter @Inject constructor( cancelJobs("load") } + fun onCalculatedAverageHelpClick() { + view?.showCalculatedAverageHelpDialog() + } + + fun onFinalAverageHelpClick() { + view?.showFinalAverageHelpDialog() + } + private fun createGradeSummaryItems(items: List): List { return items .filter { !checkEmpty(it) } 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 974d9141..156731c3 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 @@ -33,6 +33,10 @@ interface GradeSummaryView : BaseView { fun showEmpty(show: Boolean) + fun showCalculatedAverageHelpDialog() + + fun showFinalAverageHelpDialog() + fun notifyParentDataLoaded(semesterId: Int) fun notifyParentRefresh() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkFragment.kt index 1d9434dc..d4eaade2 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkFragment.kt @@ -10,6 +10,7 @@ import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Homework import io.github.wulkanowy.databinding.FragmentHomeworkBinding import io.github.wulkanowy.ui.base.BaseFragment +import io.github.wulkanowy.ui.modules.homework.add.HomeworkAddDialog import io.github.wulkanowy.ui.modules.homework.details.HomeworkDetailsDialog import io.github.wulkanowy.ui.modules.main.MainActivity import io.github.wulkanowy.ui.modules.main.MainView @@ -64,7 +65,9 @@ class HomeworkFragment : BaseFragment(R.layout.fragment homeworkPreviousButton.setOnClickListener { presenter.onPreviousDay() } homeworkNextButton.setOnClickListener { presenter.onNextDay() } - homeworkNavContainer.setElevationCompat(requireContext().dpToPx(8f)) + openAddHomeworkButton.setOnClickListener { presenter.onHomeworkAddButtonClicked() } + + homeworkNavContainer.elevation = requireContext().dpToPx(8f) } } @@ -122,10 +125,14 @@ class HomeworkFragment : BaseFragment(R.layout.fragment binding.homeworkNextButton.visibility = if (show) VISIBLE else View.INVISIBLE } - override fun showTimetableDialog(homework: Homework) { + override fun showHomeworkDialog(homework: Homework) { (activity as? MainActivity)?.showDialogFragment(HomeworkDetailsDialog.newInstance(homework)) } + override fun showAddHomeworkDialog() { + (activity as? MainActivity)?.showDialogFragment(HomeworkAddDialog()) + } + override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) outState.putLong(SAVED_DATE_KEY, presenter.currentDate.toEpochDay()) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkPresenter.kt index 11c54dc2..d7d5d7cb 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkPresenter.kt @@ -78,7 +78,11 @@ class HomeworkPresenter @Inject constructor( fun onHomeworkItemSelected(homework: Homework) { Timber.i("Select homework item ${homework.id}") - view?.showTimetableDialog(homework) + view?.showHomeworkDialog(homework) + } + + fun onHomeworkAddButtonClicked() { + view?.showAddHomeworkDialog() } private fun setBaseDateOnHolidays() { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkView.kt index a1d6a04a..7c05ab86 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkView.kt @@ -33,5 +33,7 @@ interface HomeworkView : BaseView { fun showNextButton(show: Boolean) - fun showTimetableDialog(homework: Homework) + fun showHomeworkDialog(homework: Homework) + + fun showAddHomeworkDialog() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/add/HomeworkAddDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/add/HomeworkAddDialog.kt new file mode 100644 index 00000000..12168f14 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/add/HomeworkAddDialog.kt @@ -0,0 +1,124 @@ +package io.github.wulkanowy.ui.modules.homework.add + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.widget.doOnTextChanged +import com.google.android.material.datepicker.CalendarConstraints +import com.google.android.material.datepicker.MaterialDatePicker +import dagger.hilt.android.AndroidEntryPoint +import io.github.wulkanowy.R +import io.github.wulkanowy.databinding.DialogHomeworkAddBinding +import io.github.wulkanowy.ui.base.BaseDialogFragment +import io.github.wulkanowy.utils.toFormattedString +import io.github.wulkanowy.utils.toLocalDateTime +import io.github.wulkanowy.utils.toTimestamp +import java.time.LocalDate +import javax.inject.Inject + +@AndroidEntryPoint +class HomeworkAddDialog : BaseDialogFragment(), HomeworkAddView { + + @Inject + lateinit var presenter: HomeworkAddPresenter + + private var date: LocalDate? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setStyle(STYLE_NO_TITLE, 0) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ) = DialogHomeworkAddBinding.inflate(inflater).apply { binding = this }.root + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + presenter.onAttachView(this) + } + + override fun initView() { + with(binding) { + homeworkDialogSubjectEdit.doOnTextChanged { _, _, _, _ -> + homeworkDialogSubject.error = null + homeworkDialogSubject.isErrorEnabled = false + } + homeworkDialogDateEdit.doOnTextChanged { _, _, _, _ -> + homeworkDialogDate.error = null + homeworkDialogDate.isErrorEnabled = false + } + homeworkDialogContentEdit.doOnTextChanged { _, _, _, _ -> + homeworkDialogContent.error = null + homeworkDialogContent.isErrorEnabled = false + } + homeworkDialogClose.setOnClickListener { dismiss() } + homeworkDialogDateEdit.setOnClickListener { presenter.showDatePicker(date) } + homeworkDialogAdd.setOnClickListener { + presenter.onAddHomeworkClicked( + subject = homeworkDialogSubjectEdit.text?.toString(), + teacher = homeworkDialogTeacherEdit.text?.toString(), + date = homeworkDialogDateEdit.text?.toString(), + content = homeworkDialogContentEdit.text?.toString() + ) + } + } + } + + override fun showSuccessMessage() { + showMessage(getString(R.string.homework_add_success)) + } + + override fun setErrorSubjectRequired() { + with(binding.homeworkDialogSubject) { + isErrorEnabled = true + error = getString(R.string.error_field_required) + } + } + + override fun setErrorDateRequired() { + with(binding.homeworkDialogDate) { + isErrorEnabled = true + error = getString(R.string.error_field_required) + } + } + + override fun setErrorContentRequired() { + with(binding.homeworkDialogContent) { + isErrorEnabled = true + error = getString(R.string.error_field_required) + } + } + + override fun closeDialog() { + dismiss() + } + + override fun showDatePickerDialog(currentDate: LocalDate) { + val constraintsBuilder = CalendarConstraints.Builder().apply { + setStart(LocalDate.now().toEpochDay()) + } + val datePicker = + MaterialDatePicker.Builder.datePicker() + .setCalendarConstraints(constraintsBuilder.build()) + .setSelection(currentDate.toTimestamp()) + .build() + + datePicker.addOnPositiveButtonClickListener { + date = it.toLocalDateTime().toLocalDate() + binding.homeworkDialogDate.editText?.setText(date!!.toFormattedString()) + } + + if (!parentFragmentManager.isStateSaved) { + datePicker.show(this.parentFragmentManager, null) + } + } + + override fun onDestroyView() { + presenter.onDetachView() + super.onDestroyView() + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/add/HomeworkAddPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/add/HomeworkAddPresenter.kt new file mode 100644 index 00000000..3639c2fe --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/add/HomeworkAddPresenter.kt @@ -0,0 +1,92 @@ +package io.github.wulkanowy.ui.modules.homework.add + +import io.github.wulkanowy.data.Status +import io.github.wulkanowy.data.db.entities.Homework +import io.github.wulkanowy.data.repositories.HomeworkRepository +import io.github.wulkanowy.data.repositories.SemesterRepository +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.flowWithResource +import io.github.wulkanowy.utils.toLocalDate +import kotlinx.coroutines.flow.onEach +import timber.log.Timber +import java.time.LocalDate +import javax.inject.Inject + +class HomeworkAddPresenter @Inject constructor( + errorHandler: ErrorHandler, + studentRepository: StudentRepository, + private val homeworkRepository: HomeworkRepository, + private val semesterRepository: SemesterRepository +) : BasePresenter(errorHandler, studentRepository) { + + override fun onAttachView(view: HomeworkAddView) { + super.onAttachView(view) + view.initView() + Timber.i("Homework details view was initialized") + } + + fun showDatePicker(date: LocalDate?) { + view?.showDatePickerDialog(date ?: LocalDate.now()) + } + + fun onAddHomeworkClicked(subject: String?, teacher: String?, date: String?, content: String?) { + var isError = false + + if (subject.isNullOrBlank()) { + view?.setErrorSubjectRequired() + isError = true + } + + if (date.isNullOrBlank()) { + view?.setErrorDateRequired() + isError = true + } + + if (content.isNullOrBlank()) { + view?.setErrorContentRequired() + isError = true + } + + if (!isError) { + saveHomework(subject!!, teacher.orEmpty(), date!!.toLocalDate(), content!!) + } + } + + private fun saveHomework(subject: String, teacher: String, date: LocalDate, content: String) { + flowWithResource { + val student = studentRepository.getCurrentStudent() + val semester = semesterRepository.getCurrentSemester(student) + val entryDate = LocalDate.now() + homeworkRepository.saveHomework( + Homework( + semesterId = semester.semesterId, + studentId = student.studentId, + date = date, + entryDate = entryDate, + subject = subject, + content = content, + teacher = teacher, + teacherSymbol = "", + attachments = emptyList(), + ).apply { isAddedByUser = true } + ) + }.onEach { + when (it.status) { + Status.LOADING -> Timber.i("Homework insert start") + Status.SUCCESS -> { + Timber.i("Homework insert: Success") + view?.run { + showSuccessMessage() + closeDialog() + } + } + Status.ERROR -> { + Timber.i("Homework insert result: An exception occurred") + errorHandler.dispatch(it.error!!) + } + } + }.launch("add_homework") + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/add/HomeworkAddView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/add/HomeworkAddView.kt new file mode 100644 index 00000000..3bb304d9 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/add/HomeworkAddView.kt @@ -0,0 +1,21 @@ +package io.github.wulkanowy.ui.modules.homework.add + +import io.github.wulkanowy.ui.base.BaseView +import java.time.LocalDate + +interface HomeworkAddView : BaseView { + + fun initView() + + fun showSuccessMessage() + + fun setErrorSubjectRequired() + + fun setErrorDateRequired() + + fun setErrorContentRequired() + + fun closeDialog() + + fun showDatePickerDialog(currentDate: LocalDate) +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsAdapter.kt index cd9a7e85..e03707a5 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsAdapter.kt @@ -5,10 +5,12 @@ import android.view.View.GONE import android.view.View.VISIBLE import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView +import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Homework import io.github.wulkanowy.databinding.ItemHomeworkDialogAttachmentBinding import io.github.wulkanowy.databinding.ItemHomeworkDialogAttachmentsHeaderBinding import io.github.wulkanowy.databinding.ItemHomeworkDialogDetailsBinding +import io.github.wulkanowy.utils.ifNullOrBlank import io.github.wulkanowy.utils.toFormattedString import javax.inject.Inject @@ -37,6 +39,8 @@ class HomeworkDetailsAdapter @Inject constructor() : var onFullScreenExitClickListener = {} + var onDeleteClickListener: (homework: Homework) -> Unit = {} + override fun getItemCount() = 1 + if (attachments.isNotEmpty()) attachments.size + 1 else 0 override fun getItemViewType(position: Int) = when (position) { @@ -49,9 +53,15 @@ class HomeworkDetailsAdapter @Inject constructor() : val inflater = LayoutInflater.from(parent.context) return when (viewType) { - ViewType.ATTACHMENTS_HEADER.id -> AttachmentsHeaderViewHolder(ItemHomeworkDialogAttachmentsHeaderBinding.inflate(inflater, parent, false)) - ViewType.ATTACHMENT.id -> AttachmentViewHolder(ItemHomeworkDialogAttachmentBinding.inflate(inflater, parent, false)) - else -> DetailsViewHolder(ItemHomeworkDialogDetailsBinding.inflate(inflater, parent, false)) + ViewType.ATTACHMENTS_HEADER.id -> AttachmentsHeaderViewHolder( + ItemHomeworkDialogAttachmentsHeaderBinding.inflate(inflater, parent, false) + ) + ViewType.ATTACHMENT.id -> AttachmentViewHolder( + ItemHomeworkDialogAttachmentBinding.inflate(inflater, parent, false) + ) + else -> DetailsViewHolder( + ItemHomeworkDialogDetailsBinding.inflate(inflater, parent, false) + ) } } @@ -63,12 +73,15 @@ class HomeworkDetailsAdapter @Inject constructor() : } private fun bindDetailsViewHolder(holder: DetailsViewHolder) { + val noDataString = holder.binding.root.context.getString(R.string.all_no_data) + with(holder.binding) { homeworkDialogDate.text = homework?.date?.toFormattedString() homeworkDialogEntryDate.text = homework?.entryDate?.toFormattedString() - homeworkDialogSubject.text = homework?.subject - homeworkDialogTeacher.text = homework?.teacher - homeworkDialogContent.text = homework?.content + homeworkDialogSubject.text = homework?.subject.ifNullOrBlank { noDataString } + homeworkDialogTeacher.text = homework?.teacher.ifNullOrBlank { noDataString } + homeworkDialogContent.text = homework?.content.ifNullOrBlank { noDataString } + homeworkDialogDelete.visibility = if (homework?.isAddedByUser == true) VISIBLE else GONE homeworkDialogFullScreen.visibility = if (isHomeworkFullscreen) GONE else VISIBLE homeworkDialogFullScreenExit.visibility = if (isHomeworkFullscreen) VISIBLE else GONE homeworkDialogFullScreen.setOnClickListener { @@ -81,6 +94,9 @@ class HomeworkDetailsAdapter @Inject constructor() : homeworkDialogFullScreenExit.visibility = GONE onFullScreenExitClickListener() } + homeworkDialogDelete.setOnClickListener { + onDeleteClickListener(homework!!) + } } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsDialog.kt index 93045a48..f9d46351 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsDialog.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsDialog.kt @@ -25,6 +25,9 @@ class HomeworkDetailsDialog : BaseDialogFragment(), Homew @Inject lateinit var detailsAdapter: HomeworkDetailsAdapter + override val homeworkDeleteSuccess: String + get() = getString(R.string.homework_delete_success) + private lateinit var homework: Homework companion object { @@ -82,12 +85,17 @@ class HomeworkDetailsDialog : BaseDialogFragment(), Homew dialog?.window?.setLayout(WRAP_CONTENT, WRAP_CONTENT) presenter.isHomeworkFullscreen = false } + onDeleteClickListener = { homework -> presenter.deleteHomework(homework) } isHomeworkFullscreen = presenter.isHomeworkFullscreen homework = this@HomeworkDetailsDialog.homework } } } + override fun closeDialog() { + dismiss() + } + override fun updateMarkAsDoneLabel(isDone: Boolean) { binding.homeworkDialogRead.text = view?.context?.getString(if (isDone) R.string.homework_mark_as_undone else R.string.homework_mark_as_done) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsPresenter.kt index ca6fc71e..ea9b47a0 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsPresenter.kt @@ -33,6 +33,25 @@ class HomeworkDetailsPresenter @Inject constructor( Timber.i("Homework details view was initialized") } + fun deleteHomework(homework: Homework) { + flowWithResource { homeworkRepository.deleteHomework(homework) }.onEach { + when (it.status) { + Status.LOADING -> Timber.i("Homework delete start") + Status.SUCCESS -> { + Timber.i("Homework delete: Success") + view?.run { + showMessage(homeworkDeleteSuccess) + closeDialog() + } + } + Status.ERROR -> { + Timber.i("Homework delete result: An exception occurred") + errorHandler.dispatch(it.error!!) + } + } + }.launch("delete") + } + fun toggleDone(homework: Homework) { flowWithResource { homeworkRepository.toggleDone(homework) }.onEach { when (it.status) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsView.kt index 697f2233..4a47de43 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsView.kt @@ -4,7 +4,11 @@ import io.github.wulkanowy.ui.base.BaseView interface HomeworkDetailsView : BaseView { + val homeworkDeleteSuccess: String + fun initView() + fun closeDialog() + fun updateMarkAsDoneLabel(isDone: Boolean) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginActivity.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginActivity.kt index 8d96a498..70b54c49 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginActivity.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginActivity.kt @@ -24,18 +24,24 @@ class LoginActivity : BaseActivity(), Logi @Inject override lateinit var presenter: LoginPresenter - private val loginAdapter = BaseFragmentPagerAdapter(supportFragmentManager) + private val pagerAdapter by lazy { + BaseFragmentPagerAdapter( + fragmentManager = supportFragmentManager, + pagesCount = 5, + lifecycle = lifecycle, + ) + } @Inject lateinit var updateHelper: UpdateHelper + override val currentViewIndex get() = binding.loginViewpager.currentItem + companion object { fun getStartIntent(context: Context) = Intent(context, LoginActivity::class.java) } - override val currentViewIndex get() = binding.loginViewpager.currentItem - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(ActivityLoginBinding.inflate(layoutInflater).apply { binding = this }.root) @@ -65,24 +71,26 @@ class LoginActivity : BaseActivity(), Logi setDisplayShowTitleEnabled(false) } - with(loginAdapter) { - containerId = binding.loginViewpager.id - addFragments( - listOf( - LoginFormFragment.newInstance(), - LoginSymbolFragment.newInstance(), - LoginStudentSelectFragment.newInstance(), - LoginAdvancedFragment.newInstance(), - LoginRecoverFragment.newInstance() - ) - ) - } - with(binding.loginViewpager) { offscreenPageLimit = 2 - adapter = loginAdapter + adapter = pagerAdapter + isUserInputEnabled = false setOnSelectPageListener(presenter::onViewSelected) } + + with(pagerAdapter) { + containerId = binding.loginViewpager.id + itemFactory = { + when (it) { + 0 -> LoginFormFragment.newInstance() + 1 -> LoginSymbolFragment.newInstance() + 2 -> LoginStudentSelectFragment.newInstance() + 3 -> LoginAdvancedFragment.newInstance() + 4 -> LoginRecoverFragment.newInstance() + else -> throw IllegalStateException() + } + } + } } override fun onOptionsItemSelected(item: MenuItem): Boolean { @@ -103,13 +111,12 @@ class LoginActivity : BaseActivity(), Logi } override fun notifyInitSymbolFragment(loginData: Triple) { - (loginAdapter.getFragmentInstance(1) as? LoginSymbolFragment)?.onParentInitSymbolFragment( - loginData - ) + (pagerAdapter.getFragmentInstance(1) as? LoginSymbolFragment) + ?.onParentInitSymbolFragment(loginData) } override fun notifyInitStudentSelectFragment(studentsWithSemesters: List) { - (loginAdapter.getFragmentInstance(2) as? LoginStudentSelectFragment) + (pagerAdapter.getFragmentInstance(2) as? LoginStudentSelectFragment) ?.onParentInitStudentSelectFragment(studentsWithSemesters) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginErrorHandler.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginErrorHandler.kt index ed456324..ea7215ce 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginErrorHandler.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginErrorHandler.kt @@ -1,7 +1,8 @@ package io.github.wulkanowy.ui.modules.login -import android.content.res.Resources +import android.content.Context import android.database.sqlite.SQLiteConstraintException +import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.sdk.mobile.exception.InvalidPinException import io.github.wulkanowy.sdk.mobile.exception.InvalidSymbolException @@ -11,9 +12,10 @@ import io.github.wulkanowy.sdk.scrapper.login.BadCredentialsException import io.github.wulkanowy.ui.base.ErrorHandler import javax.inject.Inject -class LoginErrorHandler @Inject constructor(resources: Resources) : ErrorHandler(resources) { +class LoginErrorHandler @Inject constructor(@ApplicationContext context: Context) : + ErrorHandler(context) { - var onBadCredentials: () -> Unit = {} + var onBadCredentials: (String?) -> Unit = {} var onInvalidToken: (String) -> Unit = {} @@ -24,8 +26,9 @@ class LoginErrorHandler @Inject constructor(resources: Resources) : ErrorHandler var onStudentDuplicate: (String) -> Unit = {} override fun proceed(error: Throwable) { + val resources = context.resources when (error) { - is BadCredentialsException -> onBadCredentials() + is BadCredentialsException -> onBadCredentials(error.message) is SQLiteConstraintException -> onStudentDuplicate(resources.getString(R.string.login_duplicate_student)) is TokenDeadException -> onInvalidToken(resources.getString(R.string.login_expired_token)) is InvalidTokenException -> onInvalidToken(resources.getString(R.string.login_invalid_token)) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedFragment.kt index 9231914c..bc29cd14 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedFragment.kt @@ -51,10 +51,12 @@ class LoginAdvancedFragment : private lateinit var hostSymbols: Array override val formHostValue: String - get() = hostValues.getOrNull(hostKeys.indexOf(binding.loginFormHost.text.toString())).orEmpty() + get() = hostValues.getOrNull(hostKeys.indexOf(binding.loginFormHost.text.toString())) + .orEmpty() override val formHostSymbol: String - get() = hostSymbols.getOrNull(hostKeys.indexOf(binding.loginFormHost.text.toString())).orEmpty() + get() = hostSymbols.getOrNull(hostKeys.indexOf(binding.loginFormHost.text.toString())) + .orEmpty() override val formPinValue: String get() = binding.loginFormPin.text.toString().trim() @@ -92,39 +94,62 @@ class LoginAdvancedFragment : loginFormSignIn.setOnClickListener { presenter.onSignInClick() } loginTypeSwitch.setOnCheckedChangeListener { _, checkedId -> - presenter.onLoginModeSelected(when (checkedId) { - R.id.loginTypeApi -> Sdk.Mode.API - R.id.loginTypeScrapper -> Sdk.Mode.SCRAPPER - else -> Sdk.Mode.HYBRID - }) + presenter.onLoginModeSelected( + when (checkedId) { + R.id.loginTypeApi -> Sdk.Mode.API + R.id.loginTypeScrapper -> Sdk.Mode.SCRAPPER + else -> Sdk.Mode.HYBRID + } + ) } loginFormPin.setOnEditorDoneSignIn { loginFormSignIn.callOnClick() } loginFormPass.setOnEditorDoneSignIn { loginFormSignIn.callOnClick() } - loginFormSymbol.setAdapter(ArrayAdapter(requireContext(), android.R.layout.simple_list_item_1, resources.getStringArray(R.array.symbols_values))) + loginFormSymbol.setAdapter( + ArrayAdapter( + requireContext(), + android.R.layout.simple_list_item_1, + resources.getStringArray(R.array.symbols_values) + ) + ) } with(binding.loginFormHost) { setText(hostKeys.getOrNull(0).orEmpty()) - setAdapter(LoginSymbolAdapter(context, R.layout.support_simple_spinner_dropdown_item, hostKeys)) + setAdapter( + LoginSymbolAdapter( + context, + R.layout.support_simple_spinner_dropdown_item, + hostKeys + ) + ) setOnClickListener { if (binding.loginFormContainer.visibility == GONE) dismissDropDown() } } } override fun showMobileApiWarningMessage() { - binding.loginFormAdvancedWarningInfo.text = getString(R.string.login_advanced_warning_mobile_api) + binding.loginFormAdvancedWarningInfo.text = + getString(R.string.login_advanced_warning_mobile_api) } override fun showScraperWarningMessage() { - binding.loginFormAdvancedWarningInfo.text = getString(R.string.login_advanced_warning_scraper) + binding.loginFormAdvancedWarningInfo.text = + getString(R.string.login_advanced_warning_scraper) } override fun showHybridWarningMessage() { - binding.loginFormAdvancedWarningInfo.text = getString(R.string.login_advanced_warning_hybrid) + binding.loginFormAdvancedWarningInfo.text = + getString(R.string.login_advanced_warning_hybrid) } - override fun setDefaultCredentials(username: String, pass: String, symbol: String, token: String, pin: String) { + override fun setDefaultCredentials( + username: String, + pass: String, + symbol: String, + token: String, + pin: String + ) { with(binding) { loginFormUsername.setText(username) loginFormPass.setText(pass) @@ -145,7 +170,7 @@ class LoginAdvancedFragment : override fun setErrorUsernameRequired() { with(binding.loginFormUsernameLayout) { requestFocus() - error = getString(R.string.login_field_required) + error = getString(R.string.error_field_required) } } @@ -166,7 +191,7 @@ class LoginAdvancedFragment : override fun setErrorPassRequired(focus: Boolean) { with(binding.loginFormPassLayout) { if (focus) requestFocus() - error = getString(R.string.login_field_required) + error = getString(R.string.error_field_required) } } @@ -177,17 +202,17 @@ class LoginAdvancedFragment : } } - override fun setErrorPassIncorrect() { + override fun setErrorPassIncorrect(message: String?) { with(binding.loginFormPassLayout) { requestFocus() - error = getString(R.string.login_incorrect_password) + error = message ?: getString(R.string.login_incorrect_password_default) } } override fun setErrorPinRequired() { with(binding.loginFormPinLayout) { requestFocus() - error = getString(R.string.login_field_required) + error = getString(R.string.error_field_required) } } @@ -201,7 +226,7 @@ class LoginAdvancedFragment : override fun setErrorSymbolRequired() { with(binding.loginFormSymbolLayout) { requestFocus() - error = getString(R.string.login_field_required) + error = getString(R.string.error_field_required) } } @@ -215,7 +240,7 @@ class LoginAdvancedFragment : override fun setErrorTokenRequired() { with(binding.loginFormTokenLayout) { requestFocus() - error = getString(R.string.login_field_required) + error = getString(R.string.error_field_required) } } @@ -296,11 +321,13 @@ class LoginAdvancedFragment : } override fun notifyParentAccountLogged(studentsWithSemesters: List) { - (activity as? LoginActivity)?.onFormFragmentAccountLogged(studentsWithSemesters, Triple( - binding.loginFormUsername.text.toString(), - binding.loginFormPass.text.toString(), - resources.getStringArray(R.array.hosts_values)[1] - )) + (activity as? LoginActivity)?.onFormFragmentAccountLogged( + studentsWithSemesters, Triple( + binding.loginFormUsername.text.toString(), + binding.loginFormPass.text.toString(), + resources.getStringArray(R.array.hosts_values)[1] + ) + ) } override fun onResume() { 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 891a6b0b..17d8c5ec 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 @@ -34,9 +34,9 @@ class LoginAdvancedPresenter @Inject constructor( } } - private fun onBadCredentials() { + private fun onBadCredentials(message: String?) { view?.run { - setErrorPassIncorrect() + setErrorPassIncorrect(message) showSoftKeyboard() Timber.i("Entered wrong username or password") } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedView.kt index 029a6b4d..1d2b2856 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedView.kt @@ -49,7 +49,7 @@ interface LoginAdvancedView : BaseView { fun setErrorPassInvalid(focus: Boolean) - fun setErrorPassIncorrect() + fun setErrorPassIncorrect(message: String?) fun clearUsernameError() 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 e383072e..c741da42 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 @@ -5,6 +5,7 @@ import android.os.Bundle import android.view.View import android.view.View.GONE import android.view.View.VISIBLE +import androidx.core.view.isVisible import androidx.core.widget.doOnTextChanged import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R @@ -41,10 +42,12 @@ class LoginFormFragment : BaseFragment(R.layout.fragme get() = binding.loginFormPass.text.toString() override val formHostValue: String - get() = hostValues.getOrNull(hostKeys.indexOf(binding.loginFormHost.text.toString())).orEmpty() + get() = hostValues.getOrNull(hostKeys.indexOf(binding.loginFormHost.text.toString())) + .orEmpty() override val formHostSymbol: String - get() = hostSymbols.getOrNull(hostKeys.indexOf(binding.loginFormHost.text.toString())).orEmpty() + get() = hostSymbols.getOrNull(hostKeys.indexOf(binding.loginFormHost.text.toString())) + .orEmpty() override val nicknameLabel: String get() = getString(R.string.login_nickname_hint) @@ -88,7 +91,13 @@ class LoginFormFragment : BaseFragment(R.layout.fragme with(binding.loginFormHost) { setText(hostKeys.getOrNull(0).orEmpty()) - setAdapter(LoginSymbolAdapter(context, R.layout.support_simple_spinner_dropdown_item, hostKeys)) + setAdapter( + LoginSymbolAdapter( + context, + R.layout.support_simple_spinner_dropdown_item, + hostKeys + ) + ) setOnClickListener { if (binding.loginFormContainer.visibility == GONE) dismissDropDown() } } } @@ -114,7 +123,7 @@ class LoginFormFragment : BaseFragment(R.layout.fragme override fun setErrorUsernameRequired() { with(binding.loginFormUsernameLayout) { - error = getString(R.string.login_field_required) + error = getString(R.string.error_field_required) } } @@ -132,7 +141,7 @@ class LoginFormFragment : BaseFragment(R.layout.fragme override fun setErrorPassRequired(focus: Boolean) { with(binding.loginFormPassLayout) { - error = getString(R.string.login_field_required) + error = getString(R.string.error_field_required) } } @@ -142,24 +151,35 @@ class LoginFormFragment : BaseFragment(R.layout.fragme } } - override fun setErrorPassIncorrect() { - with(binding.loginFormPassLayout) { - error = getString(R.string.login_incorrect_password) + override fun setErrorPassIncorrect(message: String?) { + with(binding) { + loginFormUsernameLayout.error = " " + loginFormPassLayout.error = " " + loginFormHostLayout.error = " " + loginFormErrorBox.text = message ?: getString(R.string.login_incorrect_password_default) + loginFormErrorBox.isVisible = true } } override fun setErrorEmailInvalid(domain: String) { with(binding.loginFormUsernameLayout) { - error = getString(R.string.login_invalid_custom_email,domain) + error = getString(R.string.login_invalid_custom_email, domain) } } override fun clearUsernameError() { binding.loginFormUsernameLayout.error = null + binding.loginFormErrorBox.isVisible = false } override fun clearPassError() { binding.loginFormPassLayout.error = null + binding.loginFormErrorBox.isVisible = false + } + + override fun clearHostError() { + binding.loginFormHostLayout.error = null + binding.loginFormErrorBox.isVisible = false } override fun showSoftKeyboard() { @@ -183,12 +203,18 @@ class LoginFormFragment : BaseFragment(R.layout.fragme binding.loginFormVersion.text = "v${appInfo.versionName}" } - override fun notifyParentAccountLogged(studentsWithSemesters: List, loginData: Triple) { + override fun notifyParentAccountLogged( + studentsWithSemesters: List, + loginData: Triple + ) { (activity as? LoginActivity)?.onFormFragmentAccountLogged(studentsWithSemesters, loginData) } override fun openPrivacyPolicyPage() { - context?.openInternetBrowser("https://wulkanowy.github.io/polityka-prywatnosci.html", ::showMessage) + context?.openInternetBrowser( + "https://wulkanowy.github.io/polityka-prywatnosci.html", + ::showMessage + ) } override fun showContact(show: Boolean) { @@ -210,7 +236,10 @@ class LoginFormFragment : BaseFragment(R.layout.fragme } override fun openFaqPage() { - context?.openInternetBrowser("https://wulkanowy.github.io/czesto-zadawane-pytania/dlaczego-nie-moge-sie-zalogowac", ::showMessage) + context?.openInternetBrowser( + "https://wulkanowy.github.io/czesto-zadawane-pytania/dlaczego-nie-moge-sie-zalogowac", + ::showMessage + ) } override fun onResume() { @@ -223,7 +252,8 @@ class LoginFormFragment : BaseFragment(R.layout.fragme chooserTitle = requireContext().getString(R.string.login_email_intent_title), email = "wulkanowyinc@gmail.com", subject = requireContext().getString(R.string.login_email_subject), - body = requireContext().getString(R.string.login_email_text, + body = requireContext().getString( + R.string.login_email_text, "${appInfo.systemManufacturer} ${appInfo.systemModel}", appInfo.systemVersion.toString(), appInfo.versionName, 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 99dcf1bb..21cdf2a0 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 @@ -30,7 +30,7 @@ class LoginFormPresenter @Inject constructor( showVersion() loginErrorHandler.onBadCredentials = { - setErrorPassIncorrect() + setErrorPassIncorrect(it.takeIf { !it.isNullOrBlank() }) showSoftKeyboard() Timber.i("Entered wrong username or password") } @@ -49,6 +49,7 @@ class LoginFormPresenter @Inject constructor( view?.apply { clearPassError() clearUsernameError() + clearHostError() if (formHostValue.contains("fakelog")) { setCredentials("jan@fakelog.cf", "jan123") } @@ -75,7 +76,10 @@ class LoginFormPresenter @Inject constructor( val usernameHost = username.substringAfter("@") hosts[usernameHost]?.let { - view?.setHost(it) + view?.run { + setHost(it) + clearHostError() + } } } } @@ -90,10 +94,10 @@ class LoginFormPresenter @Inject constructor( flowWithResource { studentRepository.getStudentsScrapper( - email, - password, - host, - symbol + email = email, + password = password, + scrapperBaseUrl = host, + symbol = symbol ) }.onEach { when (it.status) { 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 079629ef..30057355 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 @@ -37,7 +37,7 @@ interface LoginFormView : BaseView { fun setErrorPassInvalid(focus: Boolean) - fun setErrorPassIncorrect() + fun setErrorPassIncorrect(message: String?) fun setErrorEmailInvalid(domain: String) @@ -45,6 +45,8 @@ interface LoginFormView : BaseView { fun clearPassError() + fun clearHostError() + fun showSoftKeyboard() fun hideSoftKeyboard() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/recover/LoginRecoverFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/recover/LoginRecoverFragment.kt index 2e2f9f5c..a91dfb61 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/recover/LoginRecoverFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/recover/LoginRecoverFragment.kt @@ -99,7 +99,7 @@ class LoginRecoverFragment : override fun setErrorNameRequired() { with(bindingLocal.loginRecoverNameLayout) { requestFocus() - error = getString(R.string.login_field_required) + error = getString(R.string.error_field_required) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/recover/RecoverErrorHandler.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/recover/RecoverErrorHandler.kt index 8619369d..ac4c0313 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/recover/RecoverErrorHandler.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/recover/RecoverErrorHandler.kt @@ -1,13 +1,15 @@ package io.github.wulkanowy.ui.modules.login.recover -import android.content.res.Resources +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.sdk.scrapper.exception.InvalidCaptchaException import io.github.wulkanowy.sdk.scrapper.exception.InvalidEmailException import io.github.wulkanowy.sdk.scrapper.exception.NoAccountFoundException import io.github.wulkanowy.ui.base.ErrorHandler import javax.inject.Inject -class RecoverErrorHandler @Inject constructor(resources: Resources) : ErrorHandler(resources) { +class RecoverErrorHandler @Inject constructor(@ApplicationContext context: Context) : + ErrorHandler(context) { var onInvalidUsername: (String) -> Unit = {} @@ -15,7 +17,8 @@ class RecoverErrorHandler @Inject constructor(resources: Resources) : ErrorHandl override fun proceed(error: Throwable) { when (error) { - is InvalidEmailException, is NoAccountFoundException -> onInvalidUsername(error.localizedMessage.orEmpty()) + is InvalidEmailException, + is NoAccountFoundException -> onInvalidUsername(error.localizedMessage.orEmpty()) is InvalidCaptchaException -> onInvalidCaptcha(error.localizedMessage.orEmpty(), error) else -> super.proceed(error) } 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 e71fc0f6..87cb505c 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 @@ -66,7 +66,8 @@ class LoginStudentSelectFragment : } override fun openMainView() { - activity?.let { startActivity(MainActivity.getStartIntent(context = it, clear = true)) } + startActivity(MainActivity.getStartIntent(requireContext())) + requireActivity().finish() } override fun showProgress(show: Boolean) { @@ -108,7 +109,8 @@ class LoginStudentSelectFragment : chooserTitle = requireContext().getString(R.string.login_email_intent_title), email = "wulkanowyinc@gmail.com", subject = requireContext().getString(R.string.login_email_subject), - body = requireContext().getString(R.string.login_email_text, appInfo.systemModel, + body = requireContext().getString( + R.string.login_email_text, appInfo.systemModel, appInfo.systemVersion.toString(), appInfo.versionName, "Select users to log in", 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 c344bf44..f0f5586c 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 @@ -78,7 +78,9 @@ class LoginStudentSelectPresenter @Inject constructor( when (it.status) { Status.LOADING -> Timber.d("Login student select students load started") Status.SUCCESS -> view?.updateData(studentsWithSemesters.map { studentWithSemesters -> - studentWithSemesters to it.data!!.any { item -> compareStudents(studentWithSemesters.student, item.student) } + studentWithSemesters to it.data!!.any { item -> + compareStudents(studentWithSemesters.student, item.student) + } }) Status.ERROR -> { errorHandler.dispatch(it.error!!) @@ -95,35 +97,32 @@ class LoginStudentSelectPresenter @Inject constructor( } private fun registerStudents(studentsWithSemesters: List) { - flowWithResource { - val savedStudents = studentRepository.saveStudents(studentsWithSemesters) - val firstRegistered = studentsWithSemesters.first().apply { student.id = savedStudents.first() } - studentRepository.switchStudent(firstRegistered) - }.onEach { - when (it.status) { - Status.LOADING -> view?.run { - Timber.i("Registration started") - showProgress(true) - showContent(false) - } - Status.SUCCESS -> { - Timber.i("Registration result: Success") - view?.openMainView() - logRegisterEvent(studentsWithSemesters) - } - Status.ERROR -> { - Timber.i("Registration result: An exception occurred ") - view?.apply { - showProgress(false) - showContent(true) - showContact(true) + flowWithResource { studentRepository.saveStudents(studentsWithSemesters) } + .onEach { + when (it.status) { + Status.LOADING -> view?.run { + Timber.i("Registration started") + showProgress(true) + showContent(false) + } + Status.SUCCESS -> { + Timber.i("Registration result: Success") + view?.openMainView() + logRegisterEvent(studentsWithSemesters) + } + Status.ERROR -> { + Timber.i("Registration result: An exception occurred ") + view?.apply { + showProgress(false) + showContent(true) + showContact(true) + } + lastError = it.error + loginErrorHandler.dispatch(it.error!!) + logRegisterEvent(studentsWithSemesters, it.error) } - lastError = it.error - loginErrorHandler.dispatch(it.error!!) - logRegisterEvent(studentsWithSemesters, it.error) } - } - }.launch("register") + }.launch("register") } fun onDiscordClick() { @@ -134,7 +133,10 @@ class LoginStudentSelectPresenter @Inject constructor( view?.openEmail(lastError?.message.ifNullOrBlank { "empty" }) } - private fun logRegisterEvent(studentsWithSemesters: List, error: Throwable? = null) { + private fun logRegisterEvent( + studentsWithSemesters: List, + error: Throwable? = null + ) { studentsWithSemesters.forEach { student -> analytics.logEvent( "registration_student_select", diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolFragment.kt index e2c37db6..a8086935 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolFragment.kt @@ -7,6 +7,7 @@ import android.view.View.VISIBLE import android.view.inputmethod.EditorInfo.IME_ACTION_DONE import android.view.inputmethod.EditorInfo.IME_NULL import android.widget.ArrayAdapter +import androidx.core.text.parseAsHtml import androidx.core.widget.doOnTextChanged import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R @@ -58,7 +59,13 @@ class LoginSymbolFragment : setOnEditorActionListener { _, id, _ -> if (id == IME_ACTION_DONE || id == IME_NULL) loginSymbolSignIn.callOnClick() else false } - setAdapter(ArrayAdapter(context, android.R.layout.simple_list_item_1, resources.getStringArray(R.array.symbols_values))) + setAdapter( + ArrayAdapter( + context, + android.R.layout.simple_list_item_1, + resources.getStringArray(R.array.symbols_values) + ) + ) } } } @@ -67,6 +74,10 @@ class LoginSymbolFragment : presenter.onParentInitSymbolView(loginData) } + override fun setLoginToHeading(login: String) { + binding.loginSymbolHeader.text = getString(R.string.login_header_symbol, login).parseAsHtml() + } + override fun setErrorSymbolIncorrect() { binding.loginSymbolNameLayout.apply { requestFocus() @@ -77,7 +88,7 @@ class LoginSymbolFragment : override fun setErrorSymbolRequire() { binding.loginSymbolNameLayout.apply { requestFocus() - error = getString(R.string.login_field_required) + error = getString(R.string.error_field_required) } } @@ -127,7 +138,10 @@ class LoginSymbolFragment : } override fun openFaqPage() { - context?.openInternetBrowser("https://wulkanowy.github.io/czesto-zadawane-pytania/co-to-jest-symbol", ::showMessage) + context?.openInternetBrowser( + "https://wulkanowy.github.io/czesto-zadawane-pytania/co-to-jest-symbol", + ::showMessage + ) } override fun openEmail(host: String, lastError: String) { @@ -135,7 +149,8 @@ class LoginSymbolFragment : chooserTitle = requireContext().getString(R.string.login_email_intent_title), email = "wulkanowyinc@gmail.com", subject = requireContext().getString(R.string.login_email_subject), - body = requireContext().getString(R.string.login_email_text, + body = requireContext().getString( + R.string.login_email_text, "${appInfo.systemManufacturer} ${appInfo.systemModel}", appInfo.systemVersion.toString(), appInfo.versionName, 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 4593d880..7c84e1ec 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 @@ -32,6 +32,16 @@ class LoginSymbolPresenter @Inject constructor( } if (savedLoginData is Triple<*, *, *>) { loginData = savedLoginData as Triple + view.setLoginToHeading(requireNotNull(loginData?.first)) + } + } + + fun onParentInitSymbolView(loginData: Triple) { + this.loginData = loginData + view?.apply { + setLoginToHeading(loginData.first) + clearAndFocusSymbol() + showSoftKeyboard() } } @@ -47,7 +57,14 @@ class LoginSymbolPresenter @Inject constructor( return } - flowWithResource { studentRepository.getStudentsScrapper(loginData!!.first, loginData!!.second, loginData!!.third, symbol) }.onEach { + flowWithResource { + studentRepository.getStudentsScrapper( + email = loginData!!.first, + password = loginData!!.second, + scrapperBaseUrl = loginData!!.third, + symbol = symbol, + ) + }.onEach { when (it.status) { Status.LOADING -> view?.run { Timber.i("Login with symbol started") @@ -98,14 +115,6 @@ class LoginSymbolPresenter @Inject constructor( }.launch("login") } - fun onParentInitSymbolView(loginData: Triple) { - this.loginData = loginData - view?.apply { - clearAndFocusSymbol() - showSoftKeyboard() - } - } - fun onFaqClick() { view?.openFaqPage() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolView.kt index 830c77d1..75523a7c 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolView.kt @@ -9,6 +9,8 @@ interface LoginSymbolView : BaseView { fun initView() + fun setLoginToHeading(login: String) + fun setErrorSymbolIncorrect() fun setErrorSymbolRequire() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/history/LuckyNumberHistoryFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/history/LuckyNumberHistoryFragment.kt index dc141f8d..49d094b7 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/history/LuckyNumberHistoryFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/history/LuckyNumberHistoryFragment.kt @@ -65,7 +65,7 @@ class LuckyNumberHistoryFragment : luckyNumberHistoryPreviousButton.setOnClickListener { presenter.onPreviousWeek() } luckyNumberHistoryNextButton.setOnClickListener { presenter.onNextWeek() } - luckyNumberHistoryNavContainer.setElevationCompat(requireContext().dpToPx(8f)) + luckyNumberHistoryNavContainer.elevation = requireContext().dpToPx(8f) } } @@ -131,7 +131,9 @@ class LuckyNumberHistoryFragment : presenter.onDateSet(date.year, date.monthValue, date.dayOfMonth) } - datePicker.show(this@LuckyNumberHistoryFragment.parentFragmentManager, null) + if (!parentFragmentManager.isStateSaved) { + datePicker.show(parentFragmentManager, null) + } } override fun showContent(show: Boolean) { 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 49a19943..2b2d18fa 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 @@ -1,7 +1,6 @@ package io.github.wulkanowy.ui.modules.luckynumberwidget import android.app.PendingIntent -import android.app.PendingIntent.FLAG_UPDATE_CURRENT import android.appwidget.AppWidgetManager import android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT import android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH @@ -18,8 +17,9 @@ import io.github.wulkanowy.data.db.SharedPrefProvider import io.github.wulkanowy.data.exceptions.NoCurrentStudentException import io.github.wulkanowy.data.repositories.LuckyNumberRepository import io.github.wulkanowy.data.repositories.StudentRepository -import io.github.wulkanowy.ui.modules.main.MainActivity -import io.github.wulkanowy.ui.modules.main.MainView +import io.github.wulkanowy.ui.modules.Destination +import io.github.wulkanowy.ui.modules.splash.SplashActivity +import io.github.wulkanowy.utils.PendingIntentCompat import io.github.wulkanowy.utils.toFirstResult import kotlinx.coroutines.runBlocking import timber.log.Timber @@ -39,6 +39,8 @@ class LuckyNumberWidgetProvider : AppWidgetProvider() { companion object { + const val LUCKY_NUMBER_PENDING_INTENT_ID = 200 + fun getStudentWidgetKey(appWidgetId: Int) = "lucky_number_widget_student_$appWidgetId" fun getThemeWidgetKey(appWidgetId: Int) = "lucky_number_widget_theme_$appWidgetId" @@ -48,18 +50,31 @@ class LuckyNumberWidgetProvider : AppWidgetProvider() { fun getWidthWidgetKey(appWidgetId: Int) = "lucky_number_widget_width_$appWidgetId" } - override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray?) { + override fun onUpdate( + context: Context, + appWidgetManager: AppWidgetManager, + appWidgetIds: IntArray? + ) { super.onUpdate(context, appWidgetManager, appWidgetIds) appWidgetIds?.forEach { appWidgetId -> + val luckyNumber = + getLuckyNumber(sharedPref.getLong(getStudentWidgetKey(appWidgetId), 0), appWidgetId) + val appIntent = PendingIntent.getActivity( + context, + LUCKY_NUMBER_PENDING_INTENT_ID, + SplashActivity.getStartIntent(context, Destination.LuckyNumber), + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE + ) - val luckyNumber = getLuckyNumber(sharedPref.getLong(getStudentWidgetKey(appWidgetId), 0), appWidgetId) - val appIntent = PendingIntent.getActivity(context, MainView.Section.LUCKY_NUMBER.id, - MainActivity.getStartIntent(context, MainView.Section.LUCKY_NUMBER, true), FLAG_UPDATE_CURRENT) - - val remoteView = RemoteViews(context.packageName, getCorrectLayoutId(appWidgetId, context)).apply { - setTextViewText(R.id.luckyNumberWidgetNumber, luckyNumber?.luckyNumber?.toString() ?: "#") - setOnClickPendingIntent(R.id.luckyNumberWidgetContainer, appIntent) - } + val remoteView = + RemoteViews(context.packageName, getCorrectLayoutId(appWidgetId, context)) + .apply { + setTextViewText( + R.id.luckyNumberWidgetNumber, + luckyNumber?.luckyNumber?.toString() ?: "#" + ) + setOnClickPendingIntent(R.id.luckyNumberWidgetContainer, appIntent) + } setStyles(remoteView, appWidgetId) appWidgetManager.updateAppWidget(appWidgetId, remoteView) @@ -78,7 +93,12 @@ class LuckyNumberWidgetProvider : AppWidgetProvider() { } } - override fun onAppWidgetOptionsChanged(context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int, newOptions: Bundle?) { + override fun onAppWidgetOptionsChanged( + context: Context, + appWidgetManager: AppWidgetManager, + appWidgetId: Int, + newOptions: Bundle? + ) { super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions) val remoteView = RemoteViews(context.packageName, getCorrectLayoutId(appWidgetId, context)) @@ -88,8 +108,12 @@ class LuckyNumberWidgetProvider : AppWidgetProvider() { } private fun setStyles(views: RemoteViews, appWidgetId: Int, options: Bundle? = null) { - val width = options?.getInt(OPTION_APPWIDGET_MIN_WIDTH) ?: sharedPref.getLong(getWidthWidgetKey(appWidgetId), 74).toInt() - val height = options?.getInt(OPTION_APPWIDGET_MAX_HEIGHT) ?: sharedPref.getLong(getHeightWidgetKey(appWidgetId), 74).toInt() + val width = options?.getInt(OPTION_APPWIDGET_MIN_WIDTH) ?: sharedPref.getLong( + getWidthWidgetKey(appWidgetId), 74 + ).toInt() + val height = options?.getInt(OPTION_APPWIDGET_MAX_HEIGHT) ?: sharedPref.getLong( + getHeightWidgetKey(appWidgetId), 74 + ).toInt() with(sharedPref) { putLong(getWidthWidgetKey(appWidgetId), width.toLong()) @@ -112,7 +136,11 @@ class LuckyNumberWidgetProvider : AppWidgetProvider() { } } - private fun RemoteViews.setVisibility(imageTop: Boolean, imageLeft: Boolean, title: Boolean = false) { + private fun RemoteViews.setVisibility( + imageTop: Boolean, + imageLeft: Boolean, + title: Boolean = false + ) { setViewVisibility(R.id.luckyNumberWidgetImageTop, if (imageTop) VISIBLE else GONE) setViewVisibility(R.id.luckyNumberWidgetImageLeft, if (imageLeft) VISIBLE else GONE) setViewVisibility(R.id.luckyNumberWidgetTitle, if (title) VISIBLE else GONE) @@ -152,7 +180,8 @@ class LuckyNumberWidgetProvider : AppWidgetProvider() { private fun getCorrectLayoutId(appWidgetId: Int, context: Context): Int { val savedTheme = sharedPref.getLong(getThemeWidgetKey(appWidgetId), 0) - val isSystemDarkMode = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES + val isSystemDarkMode = + context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES return if (savedTheme == 1L || (savedTheme == 2L && isSystemDarkMode)) { R.layout.widget_luckynumber_dark 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 d758ac0d..d81abe34 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 @@ -1,20 +1,11 @@ package io.github.wulkanowy.ui.modules.main -import android.annotation.SuppressLint import android.content.Context import android.content.Intent -import android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK -import android.content.Intent.FLAG_ACTIVITY_NEW_TASK -import android.content.pm.ShortcutInfo -import android.content.pm.ShortcutManager -import android.graphics.drawable.Icon -import android.os.Build import android.os.Build.VERSION_CODES.P import android.os.Bundle import android.view.Menu import android.view.MenuItem -import androidx.annotation.RequiresApi -import androidx.core.content.getSystemService import androidx.core.view.ViewCompat import androidx.core.view.isVisible import androidx.fragment.app.DialogFragment @@ -30,19 +21,8 @@ import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.StudentWithSemesters 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.attendance.AttendanceFragment -import io.github.wulkanowy.ui.modules.conference.ConferenceFragment -import io.github.wulkanowy.ui.modules.dashboard.DashboardFragment -import io.github.wulkanowy.ui.modules.exam.ExamFragment -import io.github.wulkanowy.ui.modules.grade.GradeFragment -import io.github.wulkanowy.ui.modules.homework.HomeworkFragment -import io.github.wulkanowy.ui.modules.luckynumber.LuckyNumberFragment -import io.github.wulkanowy.ui.modules.message.MessageFragment -import io.github.wulkanowy.ui.modules.more.MoreFragment -import io.github.wulkanowy.ui.modules.note.NoteFragment -import io.github.wulkanowy.ui.modules.schoolannouncement.SchoolAnnouncementFragment -import io.github.wulkanowy.ui.modules.timetable.TimetableFragment import io.github.wulkanowy.utils.AnalyticsHelper import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.InAppReviewHelper @@ -83,15 +63,14 @@ class MainActivity : BaseActivity(), MainVie FragNavController(supportFragmentManager, R.id.main_fragment_container) companion object { - const val EXTRA_START_MENU = "extraStartMenu" + + private const val EXTRA_START_DESTINATION = "start_destination" fun getStartIntent( context: Context, - startMenu: MainView.Section? = null, - clear: Boolean = false + destination: Destination? = null, ) = Intent(context, MainActivity::class.java).apply { - if (clear) flags = FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_CLEAR_TASK - startMenu?.let { putExtra(EXTRA_START_MENU, it.id) } + putExtra(EXTRA_START_DESTINATION, destination) } } @@ -106,42 +85,20 @@ class MainActivity : BaseActivity(), MainVie override val currentViewSubtitle get() = (navController.currentFrag as? MainView.TitledView)?.subtitleString - override var startMenuIndex = 0 + private var savedInstanceState: Bundle? = null - override var startMenuMoreIndex = -1 - - private val moreMenuFragments = mapOf( - MainView.Section.MESSAGE.id to MessageFragment.newInstance(), - MainView.Section.EXAM.id to ExamFragment.newInstance(), - MainView.Section.HOMEWORK.id to HomeworkFragment.newInstance(), - MainView.Section.NOTE.id to NoteFragment.newInstance(), - MainView.Section.CONFERENCE.id to ConferenceFragment.newInstance(), - MainView.Section.SCHOOL_ANNOUNCEMENT.id to SchoolAnnouncementFragment.newInstance(), - MainView.Section.LUCKY_NUMBER.id to LuckyNumberFragment.newInstance(), - ) - - @SuppressLint("NewApi") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(ActivityMainBinding.inflate(layoutInflater).apply { binding = this }.root) setSupportActionBar(binding.mainToolbar) + this.savedInstanceState = savedInstanceState messageContainer = binding.mainMessageContainer updateHelper.messageContainer = binding.mainFragmentContainer - val section = MainView.Section.values() - .singleOrNull { it.id == intent.getIntExtra(EXTRA_START_MENU, -1) } - - presenter.onAttachView(this, section) - - with(navController) { - initialize(startMenuIndex, savedInstanceState) - pushFragment(moreMenuFragments[startMenuMoreIndex]) - } - - if (appInfo.systemVersion >= Build.VERSION_CODES.N_MR1) { - initShortcuts() - } + val destination = (intent.getSerializableExtra(EXTRA_START_DESTINATION) as Destination?) + ?.takeIf { savedInstanceState == null } + presenter.onAttachView(this, destination) updateHelper.checkAndInstallUpdates(this) } @@ -157,71 +114,47 @@ class MainActivity : BaseActivity(), MainVie updateHelper.onActivityResult(requestCode, resultCode) } - @RequiresApi(Build.VERSION_CODES.N_MR1) - fun initShortcuts() { - val shortcutsList = mutableListOf() - - listOf( - Triple( - getString(R.string.grade_title), - R.drawable.ic_shortcut_grade, - MainView.Section.GRADE - ), - Triple( - getString(R.string.attendance_title), - R.drawable.ic_shortcut_attendance, - MainView.Section.ATTENDANCE - ), - Triple( - getString(R.string.exam_title), - R.drawable.ic_shortcut_exam, - MainView.Section.EXAM - ), - Triple( - getString(R.string.timetable_title), - R.drawable.ic_shortcut_timetable, - MainView.Section.TIMETABLE - ) - ).forEach { (title, icon, enum) -> - shortcutsList.add( - ShortcutInfo.Builder(applicationContext, title) - .setShortLabel(title) - .setLongLabel(title) - .setIcon(Icon.createWithResource(applicationContext, icon)) - .setIntents( - arrayOf( - Intent(applicationContext, MainActivity::class.java) - .setAction(Intent.ACTION_VIEW), - Intent(applicationContext, MainActivity::class.java) - .putExtra(EXTRA_START_MENU, enum.id) - .setAction(Intent.ACTION_VIEW) - .addFlags(FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_CLEAR_TASK) - ) - ) - .build() - ) - } - - getSystemService()?.dynamicShortcuts = shortcutsList - } - - override fun onCreateOptionsMenu(menu: Menu?): Boolean { + override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.action_menu_main, menu) - accountMenu = menu?.findItem(R.id.mainMenuAccount) + accountMenu = menu.findItem(R.id.mainMenuAccount) presenter.onActionMenuCreated() return true } - @SuppressLint("NewApi") - override fun initView() { + override fun initView(startMenuIndex: Int, rootDestinations: List) { + initializeToolbar() + initializeBottomNavigation(startMenuIndex) + initializeNavController(startMenuIndex, rootDestinations) + } + + private fun initializeNavController(startMenuIndex: Int, rootDestinations: List) { + with(navController) { + setOnViewChangeListener { destinationView -> + presenter.onViewChange(destinationView) + analytics.setCurrentScreen( + this@MainActivity, + destinationView::class.java.simpleName + ) + } + fragmentHideStrategy = HIDE + rootFragments = rootDestinations.map { it.fragment } + + initialize(startMenuIndex, savedInstanceState) + } + savedInstanceState = null + } + + private fun initializeToolbar() { with(binding.mainToolbar) { stateListAnimator = null setBackgroundColor( overlayProvider.compositeOverlayWithThemeSurfaceColorIfNeeded(dpToPx(4f)) ) } + } + private fun initializeBottomNavigation(startMenuIndex: Int) { with(binding.mainBottomNav) { with(menu) { add(Menu.NONE, 0, Menu.NONE, R.string.dashboard_title) @@ -239,36 +172,6 @@ class MainActivity : BaseActivity(), MainVie setOnItemSelectedListener { presenter.onTabSelected(it.itemId, false) } setOnItemReselectedListener { presenter.onTabSelected(it.itemId, true) } } - - with(navController) { - setOnViewChangeListener { section, name -> - if (section == MainView.Section.ACCOUNT || section == MainView.Section.STUDENT_INFO) { - binding.mainBottomNav.isVisible = false - - if (appInfo.systemVersion >= P) { - window.navigationBarColor = getThemeAttrColor(R.attr.colorSurface) - } - } else { - binding.mainBottomNav.isVisible = true - - if (appInfo.systemVersion >= P) { - window.navigationBarColor = - getThemeAttrColor(android.R.attr.navigationBarColor) - } - } - - analytics.setCurrentScreen(this@MainActivity, name) - presenter.onViewChange(section) - } - fragmentHideStrategy = HIDE - rootFragments = listOf( - DashboardFragment.newInstance(), - GradeFragment.newInstance(), - AttendanceFragment.newInstance(), - TimetableFragment.newInstance(), - MoreFragment.newInstance() - ) - } } override fun onPreferenceStartFragment( @@ -317,6 +220,22 @@ class MainActivity : BaseActivity(), MainVie ViewCompat.setElevation(binding.mainToolbar, if (show) dpToPx(4f) else 0f) } + override fun showBottomNavigation(show: Boolean) { + binding.mainBottomNav.isVisible = show + + if (appInfo.systemVersion >= P) { + window.navigationBarColor = if (show) { + getThemeAttrColor(android.R.attr.navigationBarColor) + } else { + getThemeAttrColor(R.attr.colorSurface) + } + } + } + + override fun openMoreDestination(destination: Destination) { + pushView(destination.fragment) + } + override fun notifyMenuViewReselected() { (navController.currentStack?.getOrNull(0) as? MainView.MainChildView)?.onFragmentReselected() } @@ -373,6 +292,5 @@ class MainActivity : BaseActivity(), MainVie override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) navController.onSaveInstanceState(outState) - intent.removeExtra(EXTRA_START_MENU) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainPresenter.kt index 4805b5a1..c7893153 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainPresenter.kt @@ -6,10 +6,15 @@ import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.services.sync.SyncManager import io.github.wulkanowy.ui.base.BasePresenter +import io.github.wulkanowy.ui.base.BaseView import io.github.wulkanowy.ui.base.ErrorHandler -import io.github.wulkanowy.ui.modules.main.MainView.Section.GRADE -import io.github.wulkanowy.ui.modules.main.MainView.Section.MESSAGE -import io.github.wulkanowy.ui.modules.main.MainView.Section.SCHOOL +import io.github.wulkanowy.ui.modules.Destination +import io.github.wulkanowy.ui.modules.account.AccountView +import io.github.wulkanowy.ui.modules.account.accountdetails.AccountDetailsView +import io.github.wulkanowy.ui.modules.grade.GradeView +import io.github.wulkanowy.ui.modules.message.MessageView +import io.github.wulkanowy.ui.modules.schoolandteachers.SchoolAndTeachersView +import io.github.wulkanowy.ui.modules.studentinfo.StudentInfoView import io.github.wulkanowy.utils.AnalyticsHelper import io.github.wulkanowy.utils.flowWithResource import kotlinx.coroutines.flow.onEach @@ -27,19 +32,40 @@ class MainPresenter @Inject constructor( private var studentsWitSemesters: List? = null - fun onAttachView(view: MainView, initMenu: MainView.Section?) { - super.onAttachView(view) - view.apply { - getProperViewIndexes(initMenu).let { (main, more) -> - startMenuIndex = main - startMenuMoreIndex = more + private val rootDestinationTypeList = listOf( + Destination.Type.DASHBOARD, + Destination.Type.GRADE, + Destination.Type.ATTENDANCE, + Destination.Type.TIMETABLE, + Destination.Type.MORE + ) + + private val Destination?.startMenuIndex + get() = when { + this == null -> prefRepository.startMenuIndex + type in rootDestinationTypeList -> { + rootDestinationTypeList.indexOf(type) } - initView() - Timber.i("Main view was initialized with $startMenuIndex menu index and $startMenuMoreIndex more index") + else -> 4 + } + + fun onAttachView(view: MainView, initDestination: Destination?) { + super.onAttachView(view) + + val startMenuIndex = initDestination.startMenuIndex + val destinations = rootDestinationTypeList.map { + if (it == initDestination?.type) initDestination else it.defaultDestination + } + + view.initView(startMenuIndex, destinations) + if (initDestination != null && startMenuIndex == 4) { + view.openMoreDestination(initDestination) } syncManager.startPeriodicSyncWorker() - analytics.logEvent("app_open", "destination" to initMenu?.name) + + analytics.logEvent("app_open", "destination" to initDestination.toString()) + Timber.i("Main view was initialized with $initDestination") } fun onActionMenuCreated() { @@ -64,9 +90,10 @@ class MainPresenter @Inject constructor( }.launch("avatar") } - fun onViewChange(section: MainView.Section?) { + fun onViewChange(destinationView: BaseView) { view?.apply { - showActionBarElevation(section != GRADE && section != MESSAGE && section != SCHOOL) + showBottomNavigation(shouldShowBottomNavigation(destinationView)) + showActionBarElevation(shouldShowActionBarElevation(destinationView)) currentViewTitle?.let { setViewTitle(it) } currentViewSubtitle?.let { setViewSubTitle(it.ifBlank { null }) } currentStackSize?.let { @@ -76,6 +103,20 @@ class MainPresenter @Inject constructor( } } + private fun shouldShowActionBarElevation(destination: BaseView) = when (destination) { + is GradeView, + is MessageView, + is SchoolAndTeachersView -> false + else -> true + } + + private fun shouldShowBottomNavigation(destination: BaseView) = when (destination) { + is AccountView, + is StudentInfoView, + is AccountDetailsView -> false + else -> true + } + fun onAccountManagerSelected(): Boolean { if (studentsWitSemesters.isNullOrEmpty()) return true @@ -134,10 +175,4 @@ class MainPresenter @Inject constructor( view?.showStudentAvatar(currentStudent) } - - private fun getProperViewIndexes(initMenu: MainView.Section?) = when (initMenu?.id) { - in 0..3 -> initMenu!!.id to -1 - in 4..100 -> 4 to initMenu!!.id - else -> prefRepository.startMenuIndex to -1 - } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainView.kt index 8851f587..3a57fcc6 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainView.kt @@ -3,13 +3,10 @@ package io.github.wulkanowy.ui.modules.main import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.ui.base.BaseView +import io.github.wulkanowy.ui.modules.Destination interface MainView : BaseView { - var startMenuIndex: Int - - var startMenuMoreIndex: Int - val isRootView: Boolean val currentViewTitle: String? @@ -18,7 +15,7 @@ interface MainView : BaseView { val currentStackSize: Int? - fun initView() + fun initView(startMenuIndex: Int, rootDestinations: List) fun switchMenuView(position: Int) @@ -28,6 +25,8 @@ interface MainView : BaseView { fun showActionBarElevation(show: Boolean) + fun showBottomNavigation(show: Boolean) + fun notifyMenuViewReselected() fun notifyMenuViewChanged() @@ -42,6 +41,8 @@ interface MainView : BaseView { fun showInAppReview() + fun openMoreDestination(destination: Destination) + interface MainChildView { fun onFragmentReselected() @@ -57,25 +58,4 @@ interface MainView : BaseView { get() = "" set(_) {} } - - enum class Section { - DASHBOARD, - GRADE, - ATTENDANCE, - TIMETABLE, - MORE, - MESSAGE, - EXAM, - HOMEWORK, - NOTE, - CONFERENCE, - SCHOOL_ANNOUNCEMENT, - SCHOOL, - LUCKY_NUMBER, - ACCOUNT, - STUDENT_INFO, - SETTINGS; - - val id get() = ordinal - } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessageFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessageFragment.kt index 72fc627f..acf3133d 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessageFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessageFragment.kt @@ -4,6 +4,7 @@ import android.os.Bundle import android.view.View import android.view.View.INVISIBLE import android.view.View.VISIBLE +import com.google.android.material.tabs.TabLayoutMediator import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.data.enums.MessageFolder.RECEIVED @@ -26,7 +27,13 @@ class MessageFragment : BaseFragment(R.layout.fragment_m @Inject lateinit var presenter: MessagePresenter - private val pagerAdapter by lazy { BaseFragmentPagerAdapter(childFragmentManager) } + private val pagerAdapter by lazy { + BaseFragmentPagerAdapter( + fragmentManager = childFragmentManager, + pagesCount = 3, + lifecycle = lifecycle, + ) + } companion object { fun newInstance() = MessageFragment() @@ -43,26 +50,35 @@ class MessageFragment : BaseFragment(R.layout.fragment_m } override fun initView() { - with(pagerAdapter) { - containerId = binding.messageViewPager.id - addFragmentsWithTitle(mapOf( - MessageTabFragment.newInstance(RECEIVED) to getString(R.string.message_inbox), - MessageTabFragment.newInstance(SENT) to getString(R.string.message_sent), - MessageTabFragment.newInstance(TRASHED) to getString(R.string.message_trash) - )) - } - with(binding.messageViewPager) { adapter = pagerAdapter offscreenPageLimit = 2 setOnSelectPageListener(presenter::onPageSelected) } - with(binding.messageTabLayout) { - setupWithViewPager(binding.messageViewPager) - setElevationCompat(context.dpToPx(4f)) + with(pagerAdapter) { + containerId = binding.messageViewPager.id + titleFactory = { + when (it) { + 0 -> getString(R.string.message_inbox) + 1 -> getString(R.string.message_sent) + 2 -> getString(R.string.message_trash) + else -> throw IllegalStateException() + } + } + itemFactory = { + when (it) { + 0 -> MessageTabFragment.newInstance(RECEIVED) + 1 -> MessageTabFragment.newInstance(SENT) + 2 -> MessageTabFragment.newInstance(TRASHED) + else -> throw IllegalStateException() + } + } + TabLayoutMediator(binding.messageTabLayout, binding.messageViewPager, this).attach() } + binding.messageTabLayout.elevation = requireContext().dpToPx(4f) + binding.openSendMessageButton.setOnClickListener { presenter.onSendMessageButtonClicked() } } @@ -86,7 +102,8 @@ class MessageFragment : BaseFragment(R.layout.fragment_m } override fun notifyChildLoadData(index: Int, forceRefresh: Boolean) { - (pagerAdapter.getFragmentInstance(index) as? MessageTabFragment)?.onParentLoadData(forceRefresh) + (pagerAdapter.getFragmentInstance(index) as? MessageTabFragment) + ?.onParentLoadData(forceRefresh) } override fun openSendMessage() { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessagePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessagePresenter.kt index 7b8c3d0f..9e19517b 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessagePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessagePresenter.kt @@ -15,8 +15,7 @@ class MessagePresenter @Inject constructor( override fun onAttachView(view: MessageView) { super.onAttachView(view) - launch { - delay(150) + presenterScope.launch { view.initView() Timber.i("Message view was initialized") loadData() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewAdapter.kt index 421453c9..d75128be 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewAdapter.kt @@ -79,12 +79,7 @@ class MessagePreviewAdapter @Inject constructor() : val readText = when { recipientCount > 1 -> { - context.resources.getQuantityString( - R.plurals.message_read_by, - message.readBy, - message.readBy, - recipientCount - ) + context.getString(R.string.message_read_by, message.readBy, recipientCount) } message.readBy == 1 -> { context.getString(R.string.message_read, context.getString(R.string.all_yes)) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewFragment.kt index 74f8f57e..e1cc2e37 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewFragment.kt @@ -1,6 +1,5 @@ package io.github.wulkanowy.ui.modules.message.preview -import android.os.Build import android.os.Bundle import android.print.PrintAttributes import android.print.PrintManager @@ -13,7 +12,6 @@ import android.view.View.VISIBLE import android.webkit.WebResourceRequest import android.webkit.WebView import android.webkit.WebViewClient -import androidx.annotation.RequiresApi import androidx.core.content.getSystemService import androidx.recyclerview.widget.LinearLayoutManager import dagger.hilt.android.AndroidEntryPoint @@ -25,7 +23,6 @@ import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.modules.main.MainActivity import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.modules.message.send.SendMessageActivity -import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.shareText import javax.inject.Inject @@ -40,9 +37,6 @@ class MessagePreviewFragment : @Inject lateinit var previewAdapter: MessagePreviewAdapter - @Inject - lateinit var appInfo: AppInfo - private var menuReplyButton: MenuItem? = null private var menuForwardButton: MenuItem? = null @@ -140,7 +134,7 @@ class MessagePreviewFragment : menuForwardButton?.isVisible = show menuDeleteButton?.isVisible = show menuShareButton?.isVisible = show - menuPrintButton?.isVisible = show && appInfo.systemVersion >= Build.VERSION_CODES.LOLLIPOP + menuPrintButton?.isVisible = show } override fun setDeletedOptionsLabels() { @@ -175,7 +169,6 @@ class MessagePreviewFragment : context?.shareText(text, subject) } - @RequiresApi(Build.VERSION_CODES.LOLLIPOP) override fun printDocument(html: String, jobName: String) { val webView = WebView(requireContext()) webView.webViewClient = object : WebViewClient() { @@ -190,7 +183,6 @@ class MessagePreviewFragment : webView.loadDataWithBaseURL("file:///android_asset/", html, "text/HTML", "UTF-8", null) } - @RequiresApi(Build.VERSION_CODES.LOLLIPOP) private fun createWebPrintJob(webView: WebView, jobName: String) { activity?.getSystemService()?.let { printManager -> val printAdapter = webView.createPrintDocumentAdapter(jobName) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewPresenter.kt index 702e5467..eb33ee6e 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewPresenter.kt @@ -1,7 +1,6 @@ package io.github.wulkanowy.ui.modules.message.preview import android.annotation.SuppressLint -import android.os.Build import io.github.wulkanowy.data.Status import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.db.entities.MessageAttachment @@ -11,7 +10,6 @@ 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.AnalyticsHelper -import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.afterLoading import io.github.wulkanowy.utils.flowWithResource import io.github.wulkanowy.utils.flowWithResourceIn @@ -24,8 +22,7 @@ class MessagePreviewPresenter @Inject constructor( errorHandler: ErrorHandler, studentRepository: StudentRepository, private val messageRepository: MessageRepository, - private val analytics: AnalyticsHelper, - private var appInfo: AppInfo + private val analytics: AnalyticsHelper ) : BasePresenter(errorHandler, studentRepository) { var message: Message? = null @@ -112,10 +109,11 @@ class MessagePreviewPresenter @Inject constructor( fun onShare(): Boolean { message?.let { - var text = "Temat: ${it.subject.ifBlank { view?.messageNoSubjectString.orEmpty() }}\n" + when (it.sender.isNotEmpty()) { - true -> "Od: ${it.sender}\n" - false -> "Do: ${it.recipient}\n" - } + "Data: ${it.date.toFormattedString("yyyy-MM-dd HH:mm:ss")}\n\n${it.content}" + var text = + "Temat: ${it.subject.ifBlank { view?.messageNoSubjectString.orEmpty() }}\n" + when (it.sender.isNotEmpty()) { + true -> "Od: ${it.sender}\n" + false -> "Do: ${it.recipient}\n" + } + "Data: ${it.date.toFormattedString("yyyy-MM-dd HH:mm:ss")}\n\n${it.content}" attachments?.let { attachments -> if (attachments.isNotEmpty()) { @@ -127,7 +125,10 @@ class MessagePreviewPresenter @Inject constructor( } } - view?.shareText(text, "FW: ${it.subject.ifBlank { view?.messageNoSubjectString.orEmpty() }}") + view?.shareText( + text, + "FW: ${it.subject.ifBlank { view?.messageNoSubjectString.orEmpty() }}" + ) return true } return false @@ -135,7 +136,6 @@ class MessagePreviewPresenter @Inject constructor( @SuppressLint("NewApi") fun onPrint(): Boolean { - if (appInfo.systemVersion < Build.VERSION_CODES.LOLLIPOP) return false message?.let { val dateString = it.date.toFormattedString("yyyy-MM-dd HH:mm:ss") val infoContent = "

Data wysłania

$dateString
" + when { @@ -154,7 +154,9 @@ class MessagePreviewPresenter @Inject constructor( view?.apply { val html = printHTML - .replace("%SUBJECT%", it.subject.ifBlank { view?.messageNoSubjectString.orEmpty() }) + .replace( + "%SUBJECT%", + it.subject.ifBlank { view?.messageNoSubjectString.orEmpty() }) .replace("%CONTENT%", messageContent) .replace("%INFO%", infoContent) printDocument(html, jobName) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewView.kt index 583ba687..88fe77d9 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewView.kt @@ -1,7 +1,5 @@ package io.github.wulkanowy.ui.modules.message.preview -import android.os.Build -import androidx.annotation.RequiresApi import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.db.entities.MessageWithAttachment import io.github.wulkanowy.ui.base.BaseView @@ -42,8 +40,7 @@ interface MessagePreviewView : BaseView { fun shareText(text: String, subject: String) - @RequiresApi(Build.VERSION_CODES.LOLLIPOP) - fun printDocument(html: String, jobName: String) - fun popView() + + fun printDocument(html: String, jobName: String) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/RecipientChipItem.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/RecipientChipItem.kt index 26ab7f48..bd14bc89 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/RecipientChipItem.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/RecipientChipItem.kt @@ -1,10 +1,10 @@ package io.github.wulkanowy.ui.modules.message.send -import com.squareup.moshi.JsonClass import io.github.wulkanowy.data.db.entities.Recipient import io.github.wulkanowy.materialchipsinput.ChipItem +import kotlinx.serialization.Serializable -@JsonClass(generateAdapter = true) +@Serializable data class RecipientChipItem( override val title: String, diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/SendMessageActivity.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/SendMessageActivity.kt index 1432a994..70f9a9b5 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/SendMessageActivity.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/SendMessageActivity.kt @@ -118,7 +118,7 @@ class SendMessageActivity : BaseActivity + MessageTabDataItem.MessageItem(message) } - updateData(newItems, folder.id == MessageFolder.SENT.id) + val messageItemsWithHeader = + listOf(MessageTabDataItem.Header) + messageItems + + updateData(messageItemsWithHeader, folder.id == MessageFolder.SENT.id) notifyParentDataLoaded() } } @@ -157,6 +161,9 @@ class MessageTabPresenter @Inject constructor( enableSwipe(true) notifyParentDataLoaded() } + }.catch { + errorHandler.dispatch(it) + view?.notifyParentDataLoaded() }.launch() } @@ -167,19 +174,20 @@ class MessageTabPresenter @Inject constructor( setErrorDetails(message) showErrorView(true) showEmpty(false) + showProgress(false) } else showError(message, error) } } fun onSearchQueryTextChange(query: String) { - launch { + presenterScope.launch { searchChannel.send(query) } } @OptIn(FlowPreview::class) private fun initializeSearchStream() { - launch { + presenterScope.launch { searchChannel.consumeAsFlow() .debounce(250) .map { query -> diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/MobileDevicePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/MobileDevicePresenter.kt index 9591867d..53049891 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/MobileDevicePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/MobileDevicePresenter.kt @@ -11,7 +11,6 @@ import io.github.wulkanowy.utils.AnalyticsHelper import io.github.wulkanowy.utils.afterLoading import io.github.wulkanowy.utils.flowWithResource import io.github.wulkanowy.utils.flowWithResourceIn -import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import timber.log.Timber import javax.inject.Inject @@ -148,6 +147,6 @@ class MobileDevicePresenter @Inject constructor( errorHandler.dispatch(it.error!!) } } - }.launchIn(this) + }.launch("unregister") } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreFragment.kt index 2f0957c4..145b12a3 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreFragment.kt @@ -11,6 +11,7 @@ import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.modules.conference.ConferenceFragment import io.github.wulkanowy.ui.modules.exam.ExamFragment import io.github.wulkanowy.ui.modules.homework.HomeworkFragment +import io.github.wulkanowy.ui.modules.luckynumber.LuckyNumberFragment import io.github.wulkanowy.ui.modules.main.MainActivity import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.modules.message.MessageFragment @@ -66,6 +67,9 @@ class MoreFragment : BaseFragment(R.layout.fragment_more), override val examRes: Pair? get() = context?.run { getString(R.string.exam_title) to getCompatDrawable(R.drawable.ic_main_exam) } + override val luckyNumberRes: Pair? + get() = context?.run { getString(R.string.lucky_number_title) to getCompatDrawable(R.drawable.ic_more_lucky_number) } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding = FragmentMoreBinding.bind(view) @@ -128,6 +132,10 @@ class MoreFragment : BaseFragment(R.layout.fragment_more), (activity as? MainActivity)?.pushView(ExamFragment.newInstance()) } + override fun openLuckyNumberView() { + (activity as? MainActivity)?.pushView(LuckyNumberFragment.newInstance()) + } + override fun popView(depth: Int) { (activity as? MainActivity)?.popView(depth) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/more/MorePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/more/MorePresenter.kt index a2b7f204..92551d6e 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/more/MorePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/more/MorePresenter.kt @@ -31,6 +31,7 @@ class MorePresenter @Inject constructor( schoolAndTeachersRes?.first -> openSchoolAndTeachersView() mobileDevicesRes?.first -> openMobileDevicesView() settingsRes?.first -> openSettingsView() + luckyNumberRes?.first -> openLuckyNumberView() } } } @@ -48,6 +49,7 @@ class MorePresenter @Inject constructor( examRes, homeworkRes, noteRes, + luckyNumberRes, conferencesRes, schoolAnnouncementRes, schoolAndTeachersRes, diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreView.kt index c4a07bdc..cb895de2 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreView.kt @@ -23,6 +23,8 @@ interface MoreView : BaseView { val examRes: Pair? + val luckyNumberRes: Pair? + fun initView() fun updateData(data: List>) @@ -46,4 +48,6 @@ interface MoreView : BaseView { fun openMobileDevicesView() fun openExamView() + + fun openLuckyNumberView() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/note/NotePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/note/NotePresenter.kt index c441231e..10a39182 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/note/NotePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/note/NotePresenter.kt @@ -11,7 +11,6 @@ import io.github.wulkanowy.utils.AnalyticsHelper import io.github.wulkanowy.utils.afterLoading import io.github.wulkanowy.utils.flowWithResource import io.github.wulkanowy.utils.flowWithResourceIn -import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import timber.log.Timber import javax.inject.Inject @@ -123,15 +122,17 @@ class NotePresenter @Inject constructor( } private fun updateNote(note: Note) { - flowWithResource { noteRepository.updateNote(note) }.onEach { - when (it.status) { - Status.LOADING -> Timber.i("Attempt to update note ${note.id}") - Status.SUCCESS -> Timber.i("Update note result: Success") - Status.ERROR -> { - Timber.i("Update note result: An exception occurred") - errorHandler.dispatch(it.error!!) + flowWithResource { noteRepository.updateNote(note) } + .onEach { + when (it.status) { + Status.LOADING -> Timber.i("Attempt to update note ${note.id}") + Status.SUCCESS -> Timber.i("Update note result: Success") + Status.ERROR -> { + Timber.i("Update note result: An exception occurred") + errorHandler.dispatch(it.error!!) + } } } - }.launchIn(this) + .launch("update_note") } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/notificationscenter/NotificationsCenterAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/notificationscenter/NotificationsCenterAdapter.kt new file mode 100644 index 00000000..27b3637a --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/notificationscenter/NotificationsCenterAdapter.kt @@ -0,0 +1,47 @@ +package io.github.wulkanowy.ui.modules.notificationscenter + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import io.github.wulkanowy.data.db.entities.Notification +import io.github.wulkanowy.databinding.ItemNotificationsCenterBinding +import io.github.wulkanowy.services.sync.notifications.NotificationType +import io.github.wulkanowy.utils.toFormattedString +import javax.inject.Inject + +class NotificationsCenterAdapter @Inject constructor() : + ListAdapter(DiffUtilCallback()) { + + var onItemClickListener: (NotificationType) -> Unit = {} + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder( + ItemNotificationsCenterBinding.inflate(LayoutInflater.from(parent.context), parent, false) + ) + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val item = getItem(position) + + with(holder.binding) { + notificationsCenterItemTitle.text = item.title + notificationsCenterItemContent.text = item.content + notificationsCenterItemDate.text = item.date.toFormattedString("HH:mm, d MMM") + notificationsCenterItemIcon.setImageResource(item.type.icon) + + root.setOnClickListener { onItemClickListener(item.type) } + } + } + + class ViewHolder(val binding: ItemNotificationsCenterBinding) : + RecyclerView.ViewHolder(binding.root) + + private class DiffUtilCallback : DiffUtil.ItemCallback() { + + override fun areContentsTheSame(oldItem: Notification, newItem: Notification) = + oldItem == newItem + + override fun areItemsTheSame(oldItem: Notification, newItem: Notification) = + oldItem.id == newItem.id + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/notificationscenter/NotificationsCenterFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/notificationscenter/NotificationsCenterFragment.kt new file mode 100644 index 00000000..f3bbc42d --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/notificationscenter/NotificationsCenterFragment.kt @@ -0,0 +1,112 @@ +package io.github.wulkanowy.ui.modules.notificationscenter + +import android.os.Bundle +import android.view.View +import androidx.core.view.isVisible +import androidx.fragment.app.Fragment +import androidx.recyclerview.widget.LinearLayoutManager +import dagger.hilt.android.AndroidEntryPoint +import io.github.wulkanowy.R +import io.github.wulkanowy.data.db.entities.Notification +import io.github.wulkanowy.databinding.FragmentNotificationsCenterBinding +import io.github.wulkanowy.services.sync.notifications.NotificationType +import io.github.wulkanowy.ui.base.BaseFragment +import io.github.wulkanowy.ui.modules.attendance.AttendanceFragment +import io.github.wulkanowy.ui.modules.conference.ConferenceFragment +import io.github.wulkanowy.ui.modules.exam.ExamFragment +import io.github.wulkanowy.ui.modules.grade.GradeFragment +import io.github.wulkanowy.ui.modules.homework.HomeworkFragment +import io.github.wulkanowy.ui.modules.luckynumber.LuckyNumberFragment +import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.ui.modules.main.MainView +import io.github.wulkanowy.ui.modules.message.MessageFragment +import io.github.wulkanowy.ui.modules.note.NoteFragment +import io.github.wulkanowy.ui.modules.schoolannouncement.SchoolAnnouncementFragment +import io.github.wulkanowy.ui.modules.timetable.TimetableFragment +import javax.inject.Inject + +@AndroidEntryPoint +class NotificationsCenterFragment : + BaseFragment(R.layout.fragment_notifications_center), + NotificationsCenterView, MainView.TitledView { + + @Inject + lateinit var presenter: NotificationsCenterPresenter + + @Inject + lateinit var notificationsCenterAdapter: NotificationsCenterAdapter + + companion object { + + fun newInstance() = NotificationsCenterFragment() + } + + override val titleStringId: Int + get() = R.string.notifications_center_title + + override val isViewEmpty: Boolean + get() = notificationsCenterAdapter.itemCount == 0 + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding = FragmentNotificationsCenterBinding.bind(view) + presenter.onAttachView(this) + } + + override fun initView() { + notificationsCenterAdapter.onItemClickListener = { notificationType -> + notificationType.toDestinationFragment() + ?.let { (requireActivity() as MainActivity).pushView(it) } + } + + with(binding.notificationsCenterRecycler) { + layoutManager = LinearLayoutManager(context) + adapter = notificationsCenterAdapter + } + } + + override fun updateData(data: List) { + notificationsCenterAdapter.submitList(data) + } + + override fun showEmpty(show: Boolean) { + binding.notificationsCenterEmpty.isVisible = show + } + + override fun showProgress(show: Boolean) { + binding.notificationsCenterProgress.isVisible = show + } + + override fun showContent(show: Boolean) { + binding.notificationsCenterRecycler.isVisible = show + } + + override fun showErrorView(show: Boolean) { + binding.notificationCenterError.isVisible = show + } + + override fun setErrorDetails(message: String) { + binding.notificationCenterErrorMessage.text = message + } + + override fun onDestroyView() { + presenter.onDetachView() + super.onDestroyView() + } + + private fun NotificationType.toDestinationFragment(): Fragment? = when (this) { + NotificationType.NEW_CONFERENCE -> ConferenceFragment.newInstance() + NotificationType.NEW_EXAM -> ExamFragment.newInstance() + NotificationType.NEW_GRADE_DETAILS -> GradeFragment.newInstance() + NotificationType.NEW_GRADE_PREDICTED -> GradeFragment.newInstance() + NotificationType.NEW_GRADE_FINAL -> GradeFragment.newInstance() + NotificationType.NEW_HOMEWORK -> HomeworkFragment.newInstance() + NotificationType.NEW_LUCKY_NUMBER -> LuckyNumberFragment.newInstance() + NotificationType.NEW_MESSAGE -> MessageFragment.newInstance() + NotificationType.NEW_NOTE -> NoteFragment.newInstance() + NotificationType.NEW_ANNOUNCEMENT -> SchoolAnnouncementFragment.newInstance() + NotificationType.PUSH -> null + NotificationType.CHANGE_TIMETABLE -> TimetableFragment.newInstance() + NotificationType.NEW_ATTENDANCE -> AttendanceFragment.newInstance() + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/notificationscenter/NotificationsCenterPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/notificationscenter/NotificationsCenterPresenter.kt new file mode 100644 index 00000000..394c2310 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/notificationscenter/NotificationsCenterPresenter.kt @@ -0,0 +1,83 @@ +package io.github.wulkanowy.ui.modules.notificationscenter + +import io.github.wulkanowy.data.repositories.NotificationRepository +import io.github.wulkanowy.data.repositories.StudentRepository +import io.github.wulkanowy.ui.base.BasePresenter +import io.github.wulkanowy.ui.base.ErrorHandler +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import timber.log.Timber +import javax.inject.Inject + +class NotificationsCenterPresenter @Inject constructor( + private val notificationRepository: NotificationRepository, + errorHandler: ErrorHandler, + studentRepository: StudentRepository +) : BasePresenter(errorHandler, studentRepository) { + + private lateinit var lastError: Throwable + + override fun onAttachView(view: NotificationsCenterView) { + super.onAttachView(view) + view.initView() + Timber.i("Notifications centre view was initialized") + errorHandler.showErrorMessage = ::showErrorViewOnError + loadData() + } + + fun onRetry() { + view?.run { + showErrorView(false) + showProgress(true) + } + loadData() + } + + fun onDetailsClick() { + view?.showErrorDetailsDialog(lastError) + } + + private fun loadData() { + Timber.i("Loading notifications data started") + + flow { + val studentId = studentRepository.getCurrentStudent(false).id + emitAll(notificationRepository.getNotifications(studentId)) + } + .map { notificationList -> notificationList.sortedByDescending { it.date } } + .catch { Timber.i("Loading notifications result: An exception occurred") } + .onEach { + Timber.i("Loading notifications result: Success") + + if (it.isEmpty()) { + view?.run { + showContent(false) + showProgress(false) + showEmpty(true) + } + } else { + view?.run { + showContent(true) + showProgress(false) + showEmpty(false) + updateData(it) + } + } + } + .launch() + } + + private fun showErrorViewOnError(message: String, error: Throwable) { + view?.run { + if (isViewEmpty) { + lastError = error + setErrorDetails(message) + showErrorView(true) + showEmpty(false) + } else showError(message, error) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/notificationscenter/NotificationsCenterView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/notificationscenter/NotificationsCenterView.kt new file mode 100644 index 00000000..1bfbe75e --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/notificationscenter/NotificationsCenterView.kt @@ -0,0 +1,23 @@ +package io.github.wulkanowy.ui.modules.notificationscenter + +import io.github.wulkanowy.data.db.entities.Notification +import io.github.wulkanowy.ui.base.BaseView + +interface NotificationsCenterView : BaseView { + + val isViewEmpty: Boolean + + fun initView() + + fun updateData(data: List) + + fun showProgress(show: Boolean) + + fun showEmpty(show: Boolean) + + fun showContent(show: Boolean) + + fun showErrorView(show: Boolean) + + fun setErrorDetails(message: String) +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/SchoolAndTeachersFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/SchoolAndTeachersFragment.kt index c1c56961..f4fa8e01 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/SchoolAndTeachersFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/SchoolAndTeachersFragment.kt @@ -4,6 +4,7 @@ import android.os.Bundle import android.view.View import android.view.View.INVISIBLE import android.view.View.VISIBLE +import com.google.android.material.tabs.TabLayoutMediator import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.databinding.FragmentSchoolandteachersBinding @@ -24,7 +25,13 @@ class SchoolAndTeachersFragment : @Inject lateinit var presenter: SchoolAndTeachersPresenter - private val pagerAdapter by lazy { BaseFragmentPagerAdapter(childFragmentManager) } + private val pagerAdapter by lazy { + BaseFragmentPagerAdapter( + fragmentManager = childFragmentManager, + pagesCount = 2, + lifecycle = lifecycle, + ) + } companion object { fun newInstance() = SchoolAndTeachersFragment() @@ -41,24 +48,36 @@ class SchoolAndTeachersFragment : } override fun initView() { - with(pagerAdapter) { - containerId = binding.schoolandteachersViewPager.id - addFragmentsWithTitle(mapOf( - SchoolFragment.newInstance() to getString(R.string.school_title), - TeacherFragment.newInstance() to getString(R.string.teachers_title) - )) - } - with(binding.schoolandteachersViewPager) { adapter = pagerAdapter offscreenPageLimit = 2 setOnSelectPageListener(presenter::onPageSelected) } - with(binding.schoolandteachersTabLayout) { - setupWithViewPager(binding.schoolandteachersViewPager) - setElevationCompat(context.dpToPx(4f)) + with(pagerAdapter) { + containerId = binding.schoolandteachersViewPager.id + titleFactory = { + when (it) { + 0 -> getString(R.string.school_title) + 1 -> getString(R.string.teachers_title) + else -> throw IllegalStateException() + } + } + itemFactory = { + when (it) { + 0 -> SchoolFragment.newInstance() + 1 -> TeacherFragment.newInstance() + else -> throw IllegalStateException() + } + } + TabLayoutMediator( + binding.schoolandteachersTabLayout, + binding.schoolandteachersViewPager, + this + ).attach() } + + binding.schoolandteachersTabLayout.elevation = requireContext().dpToPx(4f) } override fun showContent(show: Boolean) { @@ -77,7 +96,8 @@ class SchoolAndTeachersFragment : } override fun notifyChildLoadData(index: Int, forceRefresh: Boolean) { - (pagerAdapter.getFragmentInstance(index) as? SchoolAndTeachersChildView)?.onParentLoadData(forceRefresh) + (pagerAdapter.getFragmentInstance(index) as? SchoolAndTeachersChildView) + ?.onParentLoadData(forceRefresh) } override fun onDestroyView() { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/SchoolAndTeachersPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/SchoolAndTeachersPresenter.kt index 915cc421..43823d6b 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/SchoolAndTeachersPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/SchoolAndTeachersPresenter.kt @@ -15,8 +15,7 @@ class SchoolAndTeachersPresenter @Inject constructor( override fun onAttachView(view: SchoolAndTeachersView) { super.onAttachView(view) - launch { - delay(150) + presenterScope.launch { view.initView() Timber.i("Message view was initialized") loadData() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/school/SchoolPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/school/SchoolPresenter.kt index 202d4e5d..ac8c273e 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/school/SchoolPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/school/SchoolPresenter.kt @@ -9,6 +9,7 @@ import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.utils.AnalyticsHelper import io.github.wulkanowy.utils.afterLoading import io.github.wulkanowy.utils.flowWithResourceIn +import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.onEach import timber.log.Timber import javax.inject.Inject @@ -100,6 +101,9 @@ class SchoolPresenter @Inject constructor( enableSwipe(true) notifyParentDataLoaded() } + }.catch { + errorHandler.dispatch(it) + view?.notifyParentDataLoaded() }.launch() } @@ -111,6 +115,7 @@ class SchoolPresenter @Inject constructor( showErrorView(true) showEmpty(false) showContent(false) + showProgress(false) } else showError(message, error) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/teacher/TeacherPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/teacher/TeacherPresenter.kt index c83cfe76..bd46ff0b 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/teacher/TeacherPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/teacher/TeacherPresenter.kt @@ -9,6 +9,7 @@ import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.utils.AnalyticsHelper import io.github.wulkanowy.utils.afterLoading import io.github.wulkanowy.utils.flowWithResourceIn +import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.onEach import timber.log.Timber import javax.inject.Inject @@ -85,6 +86,9 @@ class TeacherPresenter @Inject constructor( enableSwipe(true) notifyParentDataLoaded() } + }.catch { + errorHandler.dispatch(it) + view?.notifyParentDataLoaded() }.launch() } @@ -95,6 +99,7 @@ class TeacherPresenter @Inject constructor( setErrorDetails(message) showErrorView(true) showEmpty(false) + showProgress(false) } else showError(message, error) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolannouncement/SchoolAnnouncementDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolannouncement/SchoolAnnouncementDialog.kt index ed4b0ac9..7dcd51ce 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolannouncement/SchoolAnnouncementDialog.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolannouncement/SchoolAnnouncementDialog.kt @@ -38,7 +38,7 @@ class SchoolAnnouncementDialog : DialogFragment() { inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ) = DialogSchoolAnnouncementBinding.inflate(inflater).apply { binding = this }.root + ) = DialogSchoolAnnouncementBinding.inflate(inflater).also { binding = it }.root override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolannouncement/SchoolAnnouncementPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolannouncement/SchoolAnnouncementPresenter.kt index 88ad8146..d6a32e3c 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolannouncement/SchoolAnnouncementPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolannouncement/SchoolAnnouncementPresenter.kt @@ -64,6 +64,7 @@ class SchoolAnnouncementPresenter @Inject constructor( view?.run { enableSwipe(true) showRefresh(true) + showErrorView(false) showProgress(false) showContent(true) updateData(it.data) 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 2612fab3..d56cdfa7 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 @@ -6,7 +6,7 @@ import io.github.wulkanowy.R import io.github.wulkanowy.ui.modules.main.MainView import timber.log.Timber -class SettingsFragment : PreferenceFragmentCompat(), MainView.TitledView { +class SettingsFragment : PreferenceFragmentCompat(), MainView.TitledView, SettingsView { companion object { @@ -19,4 +19,16 @@ class SettingsFragment : PreferenceFragmentCompat(), MainView.TitledView { setPreferencesFromResource(R.xml.scheme_preferences, rootKey) Timber.i("Settings view was initialized") } + + override fun showError(text: String, error: Throwable) {} + + override fun showMessage(text: String) {} + + override fun showExpiredDialog() {} + + override fun openClearLoginView() {} + + override fun showErrorDetailsDialog(error: Throwable) {} + + override fun showChangePasswordSnackbar(redirectUrl: String) {} } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsView.kt new file mode 100644 index 00000000..79f91bc5 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsView.kt @@ -0,0 +1,5 @@ +package io.github.wulkanowy.ui.modules.settings + +import io.github.wulkanowy.ui.base.BaseView + +interface SettingsView : BaseView \ No newline at end of file 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 9f29731f..a2265b04 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 @@ -27,10 +27,6 @@ class AdvancedFragment : PreferenceFragmentCompat(), @Inject lateinit var lingver: Lingver - companion object { - fun newInstance() = AdvancedFragment() - } - override val titleStringId get() = R.string.pref_settings_advanced_title override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 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 a7ee800b..f603de78 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 @@ -27,10 +27,6 @@ class AppearanceFragment : PreferenceFragmentCompat(), @Inject lateinit var lingver: Lingver - companion object { - fun newInstance() = AppearanceFragment() - } - override val titleStringId get() = R.string.pref_settings_appearance_title override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 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 0fc7e68e..dd6cf0ec 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 @@ -1,6 +1,5 @@ package io.github.wulkanowy.ui.modules.settings.notifications -import android.annotation.SuppressLint import android.content.Intent import android.content.SharedPreferences import android.net.Uri @@ -10,9 +9,12 @@ import android.provider.Settings import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AlertDialog +import androidx.core.app.NotificationManagerCompat import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat +import androidx.preference.SwitchPreferenceCompat import androidx.recyclerview.widget.RecyclerView import com.thelittlefireman.appkillermanager.AppKillerManager import com.thelittlefireman.appkillermanager.exceptions.NoActionFoundException @@ -37,12 +39,23 @@ class NotificationsFragment : PreferenceFragmentCompat(), @Inject lateinit var appInfo: AppInfo - companion object { - fun newInstance() = NotificationsFragment() - } - override val titleStringId get() = R.string.pref_settings_notifications_title + override val isNotificationPermissionGranted: Boolean + get() { + val packageNameList = + NotificationManagerCompat.getEnabledListenerPackages(requireContext()) + val appPackageName = requireContext().packageName + + return appPackageName in packageNameList + } + + private val notificationSettingsContract = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + + presenter.onNotificationPermissionResult() + } + override fun initView(showDebugNotificationSwitch: Boolean) { findPreference(getString(R.string.pref_key_notification_debug))?.isVisible = showDebugNotificationSwitch @@ -57,12 +70,11 @@ class NotificationsFragment : PreferenceFragmentCompat(), } } - findPreference(getString(R.string.pref_key_notifications_system_settings))?.run { - setOnPreferenceClickListener { + findPreference(getString(R.string.pref_key_notifications_system_settings)) + ?.setOnPreferenceClickListener { presenter.onOpenSystemSettingsClicked() true } - } } override fun onCreateRecyclerView( @@ -139,7 +151,6 @@ class NotificationsFragment : PreferenceFragmentCompat(), .show() } - @SuppressLint("InlinedApi") override fun openSystemSettings() { val intent = if (appInfo.systemVersion >= Build.VERSION_CODES.O) { Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply { @@ -157,6 +168,25 @@ class NotificationsFragment : PreferenceFragmentCompat(), } } + override fun openNotificationPermissionDialog() { + AlertDialog.Builder(requireContext()) + .setTitle(getString(R.string.pref_notification_piggyback_popup_title)) + .setMessage(getString(R.string.pref_notification_piggyback_popup_description)) + .setPositiveButton(getString(R.string.pref_notification_piggyback_popup_positive)) { _, _ -> + notificationSettingsContract.launch(Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS")) + } + .setNegativeButton(android.R.string.cancel) { _, _ -> + setNotificationPiggybackPreferenceChecked(false) + } + .setOnDismissListener { setNotificationPiggybackPreferenceChecked(false) } + .show() + } + + override fun setNotificationPiggybackPreferenceChecked(isChecked: Boolean) { + findPreference(getString(R.string.pref_key_notifications_piggyback))?.isChecked = + isChecked + } + override fun onResume() { super.onResume() preferenceScreen.sharedPreferences.registerOnSharedPreferenceChangeListener(this) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsPresenter.kt index 8366d309..722ee96d 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsPresenter.kt @@ -31,6 +31,9 @@ class NotificationsPresenter @Inject constructor( ) initView(appInfo.isDebug) } + + checkNotificationPiggybackState() + Timber.i("Settings notifications view was initialized") } @@ -39,7 +42,7 @@ class NotificationsPresenter @Inject constructor( preferencesRepository.apply { when (key) { - isUpcomingLessonsNotificationsEnableKey -> { + isUpcomingLessonsNotificationsEnableKey, isUpcomingLessonsNotificationsPersistentKey -> { if (!isUpcomingLessonsNotificationsEnable) { timetableNotificationHelper.cancelNotification() } @@ -47,6 +50,11 @@ class NotificationsPresenter @Inject constructor( isDebugNotificationEnableKey -> { chuckerCollector.showNotification = isDebugNotificationEnable } + isNotificationPiggybackEnabledKey -> { + if (isNotificationPiggybackEnabled && view?.isNotificationPermissionGranted == false) { + view?.openNotificationPermissionDialog() + } + } } } analytics.logEvent("setting_changed", "name" to key) @@ -59,4 +67,18 @@ class NotificationsPresenter @Inject constructor( fun onOpenSystemSettingsClicked() { view?.openSystemSettings() } + + fun onNotificationPermissionResult() { + view?.run { + setNotificationPiggybackPreferenceChecked(isNotificationPermissionGranted) + } + } + + private fun checkNotificationPiggybackState() { + if (preferencesRepository.isNotificationPiggybackEnabled) { + view?.run { + setNotificationPiggybackPreferenceChecked(isNotificationPermissionGranted) + } + } + } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsView.kt index 2ab9b035..42862b38 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsView.kt @@ -4,6 +4,8 @@ import io.github.wulkanowy.ui.base.BaseView interface NotificationsView : BaseView { + val isNotificationPermissionGranted: Boolean + fun initView(showDebugNotificationSwitch: Boolean) fun showFixSyncDialog() @@ -11,4 +13,8 @@ interface NotificationsView : BaseView { fun openSystemSettings() fun enableNotification(notificationKey: String, enable: Boolean) + + fun openNotificationPermissionDialog() + + fun setNotificationPiggybackPreferenceChecked(isChecked: Boolean) } 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 83caa3b0..160b7c37 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 @@ -20,10 +20,6 @@ class SyncFragment : PreferenceFragmentCompat(), @Inject lateinit var presenter: SyncPresenter - companion object { - fun newInstance() = SyncFragment() - } - override val titleStringId get() = R.string.pref_settings_sync_title override val syncSuccessString get() = getString(R.string.pref_services_message_sync_success) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncPresenter.kt index 63e86a47..0d404a13 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncPresenter.kt @@ -46,7 +46,7 @@ class SyncPresenter @Inject constructor( fun onSyncNowClicked() { view?.run { syncManager.startOneTimeSyncWorker().onEach { workInfo -> - when (workInfo.state) { + when (workInfo?.state) { WorkInfo.State.ENQUEUED -> { setSyncInProgress(true) Timber.i("Setting sync now started") @@ -63,9 +63,9 @@ class SyncPresenter @Inject constructor( ) analytics.logEvent("sync_now", "status" to "failed") } - else -> Timber.d("Sync now state: ${workInfo.state}") + else -> Timber.d("Sync now state: ${workInfo?.state}") } - if (workInfo.state.isFinished) { + if (workInfo?.state?.isFinished == true) { setSyncInProgress(false) setSyncDateInView() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/splash/SplashActivity.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/splash/SplashActivity.kt index 376ef374..5c152455 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/splash/SplashActivity.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/splash/SplashActivity.kt @@ -1,29 +1,54 @@ package io.github.wulkanowy.ui.modules.splash +import android.annotation.SuppressLint +import android.content.Context +import android.content.Intent import android.os.Bundle import android.widget.Toast import android.widget.Toast.LENGTH_LONG +import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.viewbinding.ViewBinding import dagger.hilt.android.AndroidEntryPoint +import io.github.wulkanowy.services.shortcuts.ShortcutsHelper import io.github.wulkanowy.ui.base.BaseActivity +import io.github.wulkanowy.ui.modules.Destination import io.github.wulkanowy.ui.modules.login.LoginActivity import io.github.wulkanowy.ui.modules.main.MainActivity -import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.openInternetBrowser import javax.inject.Inject +@SuppressLint("CustomSplashScreen") @AndroidEntryPoint class SplashActivity : BaseActivity(), SplashView { @Inject - lateinit var appInfo: AppInfo + override lateinit var presenter: SplashPresenter @Inject - override lateinit var presenter: SplashPresenter + lateinit var shortcutsHelper: ShortcutsHelper + + companion object { + + private const val EXTRA_START_DESTINATION = "start_destination" + + private const val EXTRA_EXTERNAL_URL = "external_url" + + fun getStartIntent(context: Context, destination: Destination? = null) = + Intent(context, SplashActivity::class.java).apply { + putExtra(EXTRA_START_DESTINATION, destination) + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + } + } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - presenter.onAttachView(this, intent?.getStringExtra("external_url")) + installSplashScreen().setKeepVisibleCondition { true } + + val externalLink = intent?.getStringExtra(EXTRA_EXTERNAL_URL) + val startDestination = intent?.getSerializableExtra(EXTRA_START_DESTINATION) as Destination? + ?: shortcutsHelper.getDestination(intent) + + presenter.onAttachView(this, externalLink, startDestination) } override fun openLoginView() { @@ -31,8 +56,8 @@ class SplashActivity : BaseActivity(), SplashView finish() } - override fun openMainView() { - startActivity(MainActivity.getStartIntent(this)) + override fun openMainView(destination: Destination?) { + startActivity(MainActivity.getStartIntent(this, destination)) finish() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/splash/SplashPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/splash/SplashPresenter.kt index 03e43efa..0b740902 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/splash/SplashPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/splash/SplashPresenter.kt @@ -1,12 +1,10 @@ package io.github.wulkanowy.ui.modules.splash -import io.github.wulkanowy.data.Status 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.flowWithResource -import kotlinx.coroutines.flow.onEach -import timber.log.Timber +import io.github.wulkanowy.ui.modules.Destination +import kotlinx.coroutines.launch import javax.inject.Inject class SplashPresenter @Inject constructor( @@ -14,7 +12,7 @@ class SplashPresenter @Inject constructor( studentRepository: StudentRepository, ) : BasePresenter(errorHandler, studentRepository) { - fun onAttachView(view: SplashView, externalUrl: String?) { + fun onAttachView(view: SplashView, externalUrl: String?, startDestination: Destination?) { super.onAttachView(view) if (!externalUrl.isNullOrBlank()) { @@ -22,15 +20,16 @@ class SplashPresenter @Inject constructor( return } - flowWithResource { studentRepository.isCurrentStudentSet() }.onEach { - when (it.status) { - Status.LOADING -> Timber.d("Is current user set check started") - Status.SUCCESS -> { - if (it.data!!) view.openMainView() - else view.openLoginView() + presenterScope.launch { + runCatching { studentRepository.isCurrentStudentSet() } + .onFailure(errorHandler::dispatch) + .onSuccess { + if (it) { + view.openMainView(startDestination) + } else { + view.openLoginView() + } } - Status.ERROR -> errorHandler.dispatch(it.error!!) - } - }.launch() + } } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/splash/SplashView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/splash/SplashView.kt index a5aa1409..1c5d8bfd 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/splash/SplashView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/splash/SplashView.kt @@ -1,12 +1,13 @@ package io.github.wulkanowy.ui.modules.splash import io.github.wulkanowy.ui.base.BaseView +import io.github.wulkanowy.ui.modules.Destination interface SplashView : BaseView { fun openLoginView() - fun openMainView() + fun openMainView(destination: Destination?) fun openExternalUrlAndFinish(url: String) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableAdapter.kt index 87b3362d..2228aaf4 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableAdapter.kt @@ -1,12 +1,13 @@ package io.github.wulkanowy.ui.modules.timetable import android.graphics.Paint +import android.os.Handler +import android.os.Looper import android.view.LayoutInflater import android.view.View.GONE import android.view.View.VISIBLE import android.view.ViewGroup import android.widget.TextView -import androidx.core.view.ViewCompat import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import io.github.wulkanowy.R @@ -151,8 +152,8 @@ class TimetableAdapter @Inject constructor() : RecyclerView.Adapter { @@ -90,20 +89,26 @@ class TimetableDialog : DialogFragment() { R.attr.colorPrimary ) ) - timetableDialogChangesValue.setTextColor(requireContext().getThemeAttrColor(R.attr.colorPrimary)) + timetableDialogChangesValue.setTextColor( + requireContext().getThemeAttrColor( + R.attr.colorPrimary + ) + ) } else { timetableDialogChangesTitle.setTextColor( requireContext().getThemeAttrColor( R.attr.colorTimetableChange ) ) - timetableDialogChangesValue.setTextColor(requireContext().getThemeAttrColor(R.attr.colorTimetableChange)) + timetableDialogChangesValue.setTextColor( + requireContext().getThemeAttrColor( + R.attr.colorTimetableChange + ) + ) } timetableDialogChangesValue.text = when { canceled && !changes -> "Lekcja odwołana: $info" - changes && teacher.isNotBlank() -> "Zastępstwo: $teacher" - changes && teacher.isBlank() -> "Zastępstwo, ${info.decapitalise()}" else -> info.capitalise() } } @@ -131,6 +136,15 @@ class TimetableDialog : DialogFragment() { } } } + teacherOld.isNotBlank() && teacherOld == teacher -> { + timetableDialogTeacherValue.run { + visibility = GONE + } + timetableDialogTeacherNewValue.run { + visibility = VISIBLE + text = teacher + } + } teacher.isNotBlank() -> timetableDialogTeacherValue.text = teacher else -> { timetableDialogTeacherTitle.visibility = GONE diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableFragment.kt index 1e1084a8..07a9f6c7 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableFragment.kt @@ -44,7 +44,13 @@ class TimetableFragment : BaseFragment(R.layout.fragme companion object { private const val SAVED_DATE_KEY = "CURRENT_DATE" - fun newInstance() = TimetableFragment() + private const val ARGUMENT_DATE_KEY = "ARGUMENT_DATE" + + fun newInstance(date: LocalDate? = null) = TimetableFragment().apply { + arguments = Bundle().apply { + date?.let { putLong(ARGUMENT_DATE_KEY, it.toEpochDay()) } + } + } } override val titleStringId get() = R.string.timetable_title @@ -62,7 +68,11 @@ class TimetableFragment : BaseFragment(R.layout.fragme super.onViewCreated(view, savedInstanceState) binding = FragmentTimetableBinding.bind(view) messageContainer = binding.timetableRecycler - presenter.onAttachView(this, savedInstanceState?.getLong(SAVED_DATE_KEY)) + + val initDate = savedInstanceState?.getLong(SAVED_DATE_KEY) + ?: arguments?.getLong(ARGUMENT_DATE_KEY)?.takeUnless { it == 0L } + + presenter.onAttachView(this, initDate) } override fun initView() { @@ -87,7 +97,7 @@ class TimetableFragment : BaseFragment(R.layout.fragme timetableNavDate.setOnClickListener { presenter.onPickDate() } timetableNextButton.setOnClickListener { presenter.onNextDay() } - timetableNavContainer.setElevationCompat(requireContext().dpToPx(8f)) + timetableNavContainer.elevation = requireContext().dpToPx(8f) } } @@ -202,7 +212,9 @@ class TimetableFragment : BaseFragment(R.layout.fragme presenter.onDateSet(date.year, date.monthValue, date.dayOfMonth) } - datePicker.show(this@TimetableFragment.parentFragmentManager, null) + if (!parentFragmentManager.isStateSaved) { + datePicker.show(parentFragmentManager, null) + } } override fun openAdditionalLessonsView() { 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 fa1bbfb2..86e99398 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 @@ -149,6 +149,7 @@ class TimetablePresenter @Inject constructor( view?.run { enableSwipe(true) showRefresh(true) + showErrorView(false) showProgress(false) showContent(true) updateData(it.data!!.lessons) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/AdditionalLessonsFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/AdditionalLessonsFragment.kt index a4e1f0fc..e8fb9e44 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/AdditionalLessonsFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/AdditionalLessonsFragment.kt @@ -72,7 +72,7 @@ class AdditionalLessonsFragment : additionalLessonsNavDate.setOnClickListener { presenter.onPickDate() } additionalLessonsNextButton.setOnClickListener { presenter.onNextDay() } - additionalLessonsNavContainer.setElevationCompat(requireContext().dpToPx(8f)) + additionalLessonsNavContainer.elevation = requireContext().dpToPx(8f) } } @@ -152,7 +152,9 @@ class AdditionalLessonsFragment : presenter.onDateSet(date.year, date.monthValue, date.dayOfMonth) } - datePicker.show(this@AdditionalLessonsFragment.parentFragmentManager, null) + if (!parentFragmentManager.isStateSaved) { + datePicker.show(parentFragmentManager, null) + } } override fun onSaveInstanceState(outState: Bundle) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonsErrorHandler.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonsErrorHandler.kt index 00ba0bad..36e38fb9 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonsErrorHandler.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonsErrorHandler.kt @@ -1,11 +1,13 @@ package io.github.wulkanowy.ui.modules.timetable.completed -import android.content.res.Resources +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.sdk.scrapper.exception.FeatureDisabledException import io.github.wulkanowy.ui.base.ErrorHandler import javax.inject.Inject -class CompletedLessonsErrorHandler @Inject constructor(resources: Resources) : ErrorHandler(resources) { +class CompletedLessonsErrorHandler @Inject constructor(@ApplicationContext context: Context) : + ErrorHandler(context) { var onFeatureDisabled: () -> Unit = {} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonsFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonsFragment.kt index ad698c1c..a6b12644 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonsFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonsFragment.kt @@ -79,7 +79,7 @@ class CompletedLessonsFragment : completedLessonsNavDate.setOnClickListener { presenter.onPickDate() } completedLessonsNextButton.setOnClickListener { presenter.onNextDay() } - completedLessonsNavContainer.setElevationCompat(requireContext().dpToPx(8f)) + completedLessonsNavContainer.elevation = requireContext().dpToPx(8f) } } @@ -173,7 +173,9 @@ class CompletedLessonsFragment : presenter.onDateSet(date.year, date.monthValue, date.dayOfMonth) } - datePicker.show(this@CompletedLessonsFragment.parentFragmentManager, null) + if (!parentFragmentManager.isStateSaved) { + datePicker.show(parentFragmentManager, null) + } } override fun onSaveInstanceState(outState: Bundle) { 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 45b79b50..c8dda23c 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 @@ -173,7 +173,7 @@ class TimetableWidgetFactory( updateNotCanceledLessonNumberColor(this, lesson) updateNotCanceledSubjectColor(this, lesson) - val teacherChange = lesson.teacherOld.isNotBlank() && lesson.teacher != lesson.teacherOld + val teacherChange = lesson.teacherOld.isNotBlank() updateNotCanceledRoom(this, lesson, teacherChange) updateNotCanceledTeacher(this, lesson, teacherChange) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetProvider.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetProvider.kt index f9079b5f..0f069116 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetProvider.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetProvider.kt @@ -2,7 +2,6 @@ package io.github.wulkanowy.ui.modules.timetablewidget import android.annotation.SuppressLint import android.app.PendingIntent -import android.app.PendingIntent.FLAG_UPDATE_CURRENT import android.appwidget.AppWidgetManager import android.appwidget.AppWidgetManager.ACTION_APPWIDGET_DELETED import android.appwidget.AppWidgetManager.ACTION_APPWIDGET_UPDATE @@ -24,9 +23,10 @@ import io.github.wulkanowy.data.exceptions.NoCurrentStudentException import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.services.HiltBroadcastReceiver import io.github.wulkanowy.services.widgets.TimetableWidgetService -import io.github.wulkanowy.ui.modules.main.MainActivity -import io.github.wulkanowy.ui.modules.main.MainView +import io.github.wulkanowy.ui.modules.Destination +import io.github.wulkanowy.ui.modules.splash.SplashActivity import io.github.wulkanowy.utils.AnalyticsHelper +import io.github.wulkanowy.utils.PendingIntentCompat import io.github.wulkanowy.utils.capitalise import io.github.wulkanowy.utils.createNameInitialsDrawable import io.github.wulkanowy.utils.getCompatColor @@ -60,6 +60,8 @@ class TimetableWidgetProvider : HiltBroadcastReceiver() { companion object { + private const val TIMETABLE_PENDING_INTENT_ID = 201 + private const val EXTRA_TOGGLED_WIDGET_ID = "extraToggledWidget" private const val EXTRA_BUTTON_TYPE = "extraButtonType" @@ -165,18 +167,20 @@ class TimetableWidgetProvider : HiltBroadcastReceiver() { action = appWidgetId.toString() } val accountIntent = PendingIntent.getActivity( - context, -Int.MAX_VALUE + appWidgetId, + context, + -Int.MAX_VALUE + appWidgetId, Intent(context, TimetableWidgetConfigureActivity::class.java).apply { addFlags(FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_CLEAR_TASK) putExtra(EXTRA_APPWIDGET_ID, appWidgetId) putExtra(EXTRA_FROM_PROVIDER, true) - }, FLAG_UPDATE_CURRENT + }, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE + ) val appIntent = PendingIntent.getActivity( context, - MainView.Section.TIMETABLE.id, - MainActivity.getStartIntent(context, MainView.Section.TIMETABLE, true), - FLAG_UPDATE_CURRENT + TIMETABLE_PENDING_INTENT_ID, + SplashActivity.getStartIntent(context, Destination.Timetable()), + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE ) val remoteView = RemoteViews(context.packageName, layoutId).apply { @@ -220,16 +224,16 @@ class TimetableWidgetProvider : HiltBroadcastReceiver() { code: Int, appWidgetId: Int, buttonType: String - ): PendingIntent { - return PendingIntent.getBroadcast( - context, code, - Intent(context, TimetableWidgetProvider::class.java).apply { - action = ACTION_APPWIDGET_UPDATE - putExtra(EXTRA_BUTTON_TYPE, buttonType) - putExtra(EXTRA_TOGGLED_WIDGET_ID, appWidgetId) - }, FLAG_UPDATE_CURRENT - ) - } + ) = PendingIntent.getBroadcast( + context, + code, + Intent(context, TimetableWidgetProvider::class.java).apply { + action = ACTION_APPWIDGET_UPDATE + putExtra(EXTRA_BUTTON_TYPE, buttonType) + putExtra(EXTRA_TOGGLED_WIDGET_ID, appWidgetId) + }, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE + ) private suspend fun getStudent(studentId: Long, appWidgetId: Int) = try { val students = studentRepository.getSavedStudents(false) diff --git a/app/src/main/java/io/github/wulkanowy/ui/widgets/FittedScrollableTabLayout.kt b/app/src/main/java/io/github/wulkanowy/ui/widgets/FittedScrollableTabLayout.kt index 0f121dc5..6b7fb4aa 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/widgets/FittedScrollableTabLayout.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/widgets/FittedScrollableTabLayout.kt @@ -3,17 +3,15 @@ package io.github.wulkanowy.ui.widgets import android.content.Context import android.util.AttributeSet import android.view.ViewGroup +import com.google.android.material.tabs.TabLayout /** * @see Tabs don't fit to screen with tabmode=scrollable, Even with a Custom Tab Layout */ -class FittedScrollableTabLayout : MaterialTabLayout { - - constructor(context: Context) : super(context) - - constructor(context: Context, attrs: AttributeSet) : super(context, attrs) - - constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) +class FittedScrollableTabLayout @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : TabLayout(context, attrs) { override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { setMeasuredDimension(widthMeasureSpec, heightMeasureSpec) diff --git a/app/src/main/java/io/github/wulkanowy/ui/widgets/MaterialLinearLayout.kt b/app/src/main/java/io/github/wulkanowy/ui/widgets/MaterialLinearLayout.kt index a04922e5..4e1ca1a9 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/widgets/MaterialLinearLayout.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/widgets/MaterialLinearLayout.kt @@ -1,24 +1,19 @@ package io.github.wulkanowy.ui.widgets import android.content.Context -import android.os.Build.VERSION.SDK_INT -import android.os.Build.VERSION_CODES.LOLLIPOP import android.util.AttributeSet import android.widget.LinearLayout import androidx.core.view.ViewCompat -import com.google.android.material.elevation.ElevationOverlayProvider import com.google.android.material.shape.MaterialShapeDrawable -class MaterialLinearLayout : LinearLayout { - - constructor(context: Context) : super(context) - - constructor(context: Context, attr: AttributeSet) : super(context, attr) - - constructor(context: Context, attr: AttributeSet, defStyleAttr: Int) : super(context, attr, defStyleAttr) +class MaterialLinearLayout @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : LinearLayout(context, attrs) { init { - val drawable = MaterialShapeDrawable.createWithElevationOverlay(context, ViewCompat.getElevation(this)) + val drawable = + MaterialShapeDrawable.createWithElevationOverlay(context, ViewCompat.getElevation(this)) ViewCompat.setBackground(this, drawable) } @@ -28,12 +23,4 @@ class MaterialLinearLayout : LinearLayout { (background as MaterialShapeDrawable).elevation = elevation } } - - fun setElevationCompat(elevation: Float) { - if (SDK_INT >= LOLLIPOP) { - setElevation(elevation) - } else { - setBackgroundColor(ElevationOverlayProvider(context).compositeOverlayWithThemeSurfaceColorIfNeeded(elevation)) - } - } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/widgets/MaterialTabLayout.kt b/app/src/main/java/io/github/wulkanowy/ui/widgets/MaterialTabLayout.kt deleted file mode 100644 index e19d0111..00000000 --- a/app/src/main/java/io/github/wulkanowy/ui/widgets/MaterialTabLayout.kt +++ /dev/null @@ -1,25 +0,0 @@ -package io.github.wulkanowy.ui.widgets - -import android.content.Context -import android.os.Build.VERSION.SDK_INT -import android.os.Build.VERSION_CODES.LOLLIPOP -import android.util.AttributeSet -import com.google.android.material.elevation.ElevationOverlayProvider -import com.google.android.material.tabs.TabLayout - -open class MaterialTabLayout : TabLayout { - - constructor(context: Context) : super(context) - - constructor(context: Context, attr: AttributeSet) : super(context, attr) - - constructor(context: Context, attr: AttributeSet, defStyleAttr: Int) : super(context, attr, defStyleAttr) - - fun setElevationCompat(elevation: Float) { - if (SDK_INT >= LOLLIPOP) { - setElevation(elevation) - } else { - setBackgroundColor(ElevationOverlayProvider(context).compositeOverlayWithThemeSurfaceColorIfNeeded(elevation)) - } - } -} diff --git a/app/src/main/java/io/github/wulkanowy/ui/widgets/SwipeDisabledViewPager.kt b/app/src/main/java/io/github/wulkanowy/ui/widgets/SwipeDisabledViewPager.kt deleted file mode 100644 index eb5cae4f..00000000 --- a/app/src/main/java/io/github/wulkanowy/ui/widgets/SwipeDisabledViewPager.kt +++ /dev/null @@ -1,19 +0,0 @@ -package io.github.wulkanowy.ui.widgets - -import android.annotation.SuppressLint -import android.content.Context -import android.util.AttributeSet -import android.view.MotionEvent -import androidx.viewpager.widget.ViewPager - -class SwipeDisabledViewPager : ViewPager { - - constructor(context: Context) : super(context) - - constructor(context: Context, attr: AttributeSet) : super(context, attr) - - @SuppressLint("ClickableViewAccessibility") - override fun onTouchEvent(ev: MotionEvent) = false - - override fun onInterceptTouchEvent(ev: MotionEvent) = false -} diff --git a/app/src/main/java/io/github/wulkanowy/utils/AppInfo.kt b/app/src/main/java/io/github/wulkanowy/utils/AppInfo.kt index a3961aed..962e5b20 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/AppInfo.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/AppInfo.kt @@ -1,35 +1,31 @@ package io.github.wulkanowy.utils import android.content.res.Resources -import android.os.Build.MANUFACTURER -import android.os.Build.MODEL -import android.os.Build.VERSION.SDK_INT -import io.github.wulkanowy.BuildConfig.BUILD_TIMESTAMP -import io.github.wulkanowy.BuildConfig.DEBUG -import io.github.wulkanowy.BuildConfig.FLAVOR -import io.github.wulkanowy.BuildConfig.VERSION_CODE -import io.github.wulkanowy.BuildConfig.VERSION_NAME +import android.os.Build +import io.github.wulkanowy.BuildConfig import javax.inject.Inject import javax.inject.Singleton @Singleton open class AppInfo @Inject constructor() { - open val isDebug get() = DEBUG + open val isDebug get() = BuildConfig.DEBUG - open val versionCode get() = VERSION_CODE + open val versionCode get() = BuildConfig.VERSION_CODE - open val buildTimestamp get() = BUILD_TIMESTAMP + open val buildTimestamp get() = BuildConfig.BUILD_TIMESTAMP - open val buildFlavor get() = FLAVOR + open val buildFlavor get() = BuildConfig.FLAVOR - open val versionName get() = VERSION_NAME + open val versionName get() = BuildConfig.VERSION_NAME - open val systemVersion get() = SDK_INT + open val systemVersion get() = Build.VERSION.SDK_INT - open val systemManufacturer: String get() = MANUFACTURER + open val systemManufacturer: String get() = Build.MANUFACTURER - open val systemModel: String get() = MODEL + open val systemModel: String get() = Build.MODEL + + open val messagesBaseUrl = BuildConfig.MESSAGES_BASE_URL @Suppress("DEPRECATION") open val systemLanguage: String diff --git a/app/src/main/java/io/github/wulkanowy/utils/AttendanceExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/AttendanceExtension.kt index 479cc518..397c9595 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/AttendanceExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/AttendanceExtension.kt @@ -17,7 +17,7 @@ private inline val AttendanceSummary.allAbsences: Double get() = absence.toDouble() + absenceExcused inline val Attendance.isExcusableOrNotExcused: Boolean - get() = excusable || ((absence || lateness) && !excused) + get() = (excusable || ((absence || lateness) && !excused)) && excuseStatus == null fun AttendanceSummary.calculatePercentage() = calculatePercentage(allPresences, allAbsences) @@ -29,7 +29,7 @@ private fun calculatePercentage(presence: Double, absence: Double): Double { return if ((presence + absence) == 0.0) 0.0 else (presence / (presence + absence)) * 100 } -inline val Attendance.description +inline val Attendance.descriptionRes get() = when (AttendanceCategory.getCategoryByName(name)) { AttendanceCategory.PRESENCE -> R.string.attendance_present AttendanceCategory.ABSENCE_UNEXCUSED -> R.string.attendance_absence_unexcused diff --git a/app/src/main/java/io/github/wulkanowy/utils/ContextExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/ContextExtension.kt index 68d3afe8..ecd982a1 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/ContextExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/ContextExtension.kt @@ -18,6 +18,7 @@ import androidx.annotation.AttrRes import androidx.annotation.ColorInt import androidx.annotation.ColorRes import androidx.annotation.DrawableRes +import androidx.annotation.PluralsRes import androidx.core.content.ContextCompat import androidx.core.graphics.ColorUtils import androidx.core.graphics.applyCanvas @@ -57,6 +58,9 @@ fun Context.getCompatDrawable(@DrawableRes drawableRes: Int, @ColorRes colorRes: fun Context.getCompatBitmap(@DrawableRes drawableRes: Int, @ColorRes colorRes: Int) = getCompatDrawable(drawableRes, colorRes)?.toBitmap() +fun Context.getPlural(@PluralsRes pluralRes: Int, quantity: Int, vararg arguments: Any) = + resources.getQuantityString(pluralRes, quantity, *arguments) + fun Context.openInternetBrowser(uri: String, onActivityNotFound: (uri: String) -> Unit = {}) { Intent.parseUri(uri, 0).let { try { @@ -102,7 +106,9 @@ fun Context.openNavigation(location: String) { fun Context.openDialer(phone: String) { val intentUri = Uri.parse("tel:$phone") val intent = Intent(Intent.ACTION_DIAL, intentUri) - startActivity(intent) + if (intent.resolveActivity(packageManager) != null) { + startActivity(intent) + } } fun Context.shareText(text: String, subject: String?) { diff --git a/app/src/main/java/io/github/wulkanowy/utils/DispatchersProvider.kt b/app/src/main/java/io/github/wulkanowy/utils/DispatchersProvider.kt index ecc8e05e..8aaa57f4 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/DispatchersProvider.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/DispatchersProvider.kt @@ -1,10 +1,8 @@ package io.github.wulkanowy.utils -import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers open class DispatchersProvider { - open val backgroundThread: CoroutineDispatcher - get() = Dispatchers.IO + open val io get() = Dispatchers.IO } diff --git a/app/src/main/java/io/github/wulkanowy/utils/FragNavControlerExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/FragNavControlerExtension.kt index 9dc1e18a..01c876dd 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/FragNavControlerExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/FragNavControlerExtension.kt @@ -2,16 +2,19 @@ package io.github.wulkanowy.utils import androidx.fragment.app.Fragment import com.ncapdevi.fragnav.FragNavController -import io.github.wulkanowy.ui.modules.main.MainView +import io.github.wulkanowy.ui.base.BaseView -inline fun FragNavController.setOnViewChangeListener(crossinline listener: (section: MainView.Section?, name: String?) -> Unit) { +inline fun FragNavController.setOnViewChangeListener(crossinline listener: (view: BaseView) -> Unit) { transactionListener = object : FragNavController.TransactionListener { - override fun onFragmentTransaction(fragment: Fragment?, transactionType: FragNavController.TransactionType) { - listener(fragment?.toSection(), fragment?.let { it::class.java.simpleName }) + override fun onFragmentTransaction( + fragment: Fragment?, + transactionType: FragNavController.TransactionType + ) { + fragment?.let { listener(it as BaseView) } } override fun onTabTransaction(fragment: Fragment?, index: Int) { - listener(fragment?.toSection(), fragment?.let { it::class.java.simpleName }) + fragment?.let { listener(it as BaseView) } } } } diff --git a/app/src/main/java/io/github/wulkanowy/utils/FragmentExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/FragmentExtension.kt deleted file mode 100644 index 210a6209..00000000 --- a/app/src/main/java/io/github/wulkanowy/utils/FragmentExtension.kt +++ /dev/null @@ -1,44 +0,0 @@ -package io.github.wulkanowy.utils - -import androidx.fragment.app.Fragment -import io.github.wulkanowy.ui.modules.account.AccountFragment -import io.github.wulkanowy.ui.modules.account.accountdetails.AccountDetailsFragment -import io.github.wulkanowy.ui.modules.attendance.AttendanceFragment -import io.github.wulkanowy.ui.modules.conference.ConferenceFragment -import io.github.wulkanowy.ui.modules.dashboard.DashboardFragment -import io.github.wulkanowy.ui.modules.exam.ExamFragment -import io.github.wulkanowy.ui.modules.grade.GradeFragment -import io.github.wulkanowy.ui.modules.homework.HomeworkFragment -import io.github.wulkanowy.ui.modules.luckynumber.LuckyNumberFragment -import io.github.wulkanowy.ui.modules.main.MainView -import io.github.wulkanowy.ui.modules.message.MessageFragment -import io.github.wulkanowy.ui.modules.more.MoreFragment -import io.github.wulkanowy.ui.modules.note.NoteFragment -import io.github.wulkanowy.ui.modules.schoolandteachers.SchoolAndTeachersFragment -import io.github.wulkanowy.ui.modules.schoolannouncement.SchoolAnnouncementFragment -import io.github.wulkanowy.ui.modules.settings.SettingsFragment -import io.github.wulkanowy.ui.modules.studentinfo.StudentInfoFragment -import io.github.wulkanowy.ui.modules.timetable.TimetableFragment - -fun Fragment.toSection(): MainView.Section? { - return when (this) { - is GradeFragment -> MainView.Section.GRADE - is AttendanceFragment -> MainView.Section.ATTENDANCE - is ExamFragment -> MainView.Section.EXAM - is TimetableFragment -> MainView.Section.TIMETABLE - is MoreFragment -> MainView.Section.MORE - is MessageFragment -> MainView.Section.MESSAGE - is HomeworkFragment -> MainView.Section.HOMEWORK - is NoteFragment -> MainView.Section.NOTE - is LuckyNumberFragment -> MainView.Section.LUCKY_NUMBER - is SettingsFragment -> MainView.Section.SETTINGS - is SchoolAndTeachersFragment -> MainView.Section.SCHOOL - is AccountFragment -> MainView.Section.ACCOUNT - is AccountDetailsFragment -> MainView.Section.ACCOUNT - is StudentInfoFragment -> MainView.Section.STUDENT_INFO - is ConferenceFragment -> MainView.Section.CONFERENCE - is SchoolAnnouncementFragment -> MainView.Section.SCHOOL_ANNOUNCEMENT - is DashboardFragment -> MainView.Section.DASHBOARD - else -> null - } -} diff --git a/app/src/main/java/io/github/wulkanowy/utils/GradeExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/GradeExtension.kt index 820e7f43..1be3093f 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/GradeExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/GradeExtension.kt @@ -3,7 +3,7 @@ package io.github.wulkanowy.utils import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Grade import io.github.wulkanowy.data.db.entities.GradeSummary -import io.github.wulkanowy.sdk.scrapper.grades.* +import io.github.wulkanowy.sdk.scrapper.grades.isGradeValid fun List.calcAverage(isOptionalArithmeticAverage: Boolean): Double { val isArithmeticAverage = isOptionalArithmeticAverage && !any { it.weightValue != .0 } @@ -18,8 +18,7 @@ fun List.calcAverage(isOptionalArithmeticAverage: Boolean): Double { return if (denominator != 0.0) counter / denominator else 0.0 } -@JvmName("calcSummaryAverage") -fun List.calcAverage(plusModifier: Double, minusModifier: Double) = asSequence() +fun List.calcFinalAverage(plusModifier: Double, minusModifier: Double) = asSequence() .mapNotNull { if (it.finalGrade.matches("[0-6][+-]?".toRegex())) { when { diff --git a/app/src/main/java/io/github/wulkanowy/utils/LifecycleAwareVariable.kt b/app/src/main/java/io/github/wulkanowy/utils/LifecycleAwareVariable.kt index d2a8908c..032e2d28 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/LifecycleAwareVariable.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/LifecycleAwareVariable.kt @@ -4,13 +4,12 @@ import android.os.Handler import android.os.Looper import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleObserver -import androidx.lifecycle.OnLifecycleEvent +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner import kotlin.properties.ReadWriteProperty import kotlin.reflect.KProperty -class LifecycleAwareVariable : ReadWriteProperty, LifecycleObserver { +class LifecycleAwareVariable : ReadWriteProperty, DefaultLifecycleObserver { private var _value: T? = null @@ -23,15 +22,15 @@ class LifecycleAwareVariable : ReadWriteProperty, Lifecycl override fun getValue(thisRef: Fragment, property: KProperty<*>) = _value ?: throw IllegalStateException("Trying to call an lifecycle-aware value outside of the view lifecycle, or the value has not been initialized") - @Suppress("unused") - @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) - fun onDestroyView() { - _value = null + override fun onDestroy(owner: LifecycleOwner) { + Handler(Looper.getMainLooper()).post { + _value = null + } } } class LifecycleAwareVariableActivity : ReadWriteProperty, - LifecycleObserver { + DefaultLifecycleObserver { private var _value: T? = null @@ -44,9 +43,7 @@ class LifecycleAwareVariableActivity : ReadWriteProperty) = _value ?: throw IllegalStateException("Trying to call an lifecycle-aware value outside of the view lifecycle, or the value has not been initialized") - @Suppress("unused") - @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) - fun onDestroyView() { + override fun onDestroy(owner: LifecycleOwner) { Handler(Looper.getMainLooper()).post { _value = null } diff --git a/app/src/main/java/io/github/wulkanowy/utils/PendingIntentCompat.kt b/app/src/main/java/io/github/wulkanowy/utils/PendingIntentCompat.kt new file mode 100644 index 00000000..45ee431a --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/utils/PendingIntentCompat.kt @@ -0,0 +1,11 @@ +package io.github.wulkanowy.utils + +import android.app.PendingIntent +import android.os.Build + +object PendingIntentCompat { + + val FLAG_IMMUTABLE = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + PendingIntent.FLAG_IMMUTABLE + } else 0 +} diff --git a/app/src/main/java/io/github/wulkanowy/utils/RefreshUtils.kt b/app/src/main/java/io/github/wulkanowy/utils/RefreshUtils.kt index cd59b864..6bf97bae 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/RefreshUtils.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/RefreshUtils.kt @@ -33,7 +33,7 @@ class AutoRefreshHelper @Inject constructor( private val sharedPref: SharedPrefProvider ) { - fun isShouldBeRefreshed(key: String): Boolean { + fun shouldBeRefreshed(key: String): Boolean { val timestamp = sharedPref.getLong(key, 0).toLocalDateTime() val servicesInterval = sharedPref.getString(context.getString(R.string.pref_key_services_interval), context.getString(R.string.pref_default_services_interval)).toLong() diff --git a/app/src/main/java/io/github/wulkanowy/utils/SchooldaysValidator.kt b/app/src/main/java/io/github/wulkanowy/utils/SchooldaysValidator.kt index 00fccfc8..b6dd528f 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/SchooldaysValidator.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/SchooldaysValidator.kt @@ -2,7 +2,6 @@ package io.github.wulkanowy.utils import com.google.android.material.datepicker.CalendarConstraints import kotlinx.parcelize.Parcelize -import java.time.DayOfWeek import java.time.temporal.ChronoUnit @Parcelize @@ -12,7 +11,6 @@ class SchoolDaysValidator(val start: Long, val end: Long) : CalendarConstraints. val date = dateLong.toLocalDateTime() return date.until(end.toLocalDateTime(), ChronoUnit.DAYS) >= 0 && - date.until(start.toLocalDateTime(), ChronoUnit.DAYS) <= 0 && - date.dayOfWeek != DayOfWeek.SUNDAY + date.until(start.toLocalDateTime(), ChronoUnit.DAYS) <= 0 } -} \ No newline at end of file +} diff --git a/app/src/main/java/io/github/wulkanowy/utils/StringExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/StringExtension.kt index 5c888f30..bddd7df4 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/StringExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/StringExtension.kt @@ -4,6 +4,4 @@ inline fun String?.ifNullOrBlank(defaultValue: () -> String) = if (isNullOrBlank()) defaultValue() else this fun String.capitalise() = - replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() } - -fun String.decapitalise() = replaceFirstChar { it.lowercase() } + replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() } \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/utils/ViewPagerExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/ViewPagerExtension.kt index 6a5ad880..700ac2f1 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/ViewPagerExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/ViewPagerExtension.kt @@ -1,13 +1,11 @@ package io.github.wulkanowy.utils -import androidx.viewpager.widget.ViewPager +import androidx.viewpager2.widget.ViewPager2 -inline fun ViewPager.setOnSelectPageListener(crossinline selectListener: (position: Int) -> Unit) { - addOnPageChangeListener(object : ViewPager.OnPageChangeListener { +inline fun ViewPager2.setOnSelectPageListener(crossinline selectListener: (position: Int) -> Unit) { + registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { override fun onPageSelected(position: Int) { selectListener(position) } - override fun onPageScrollStateChanged(state: Int) {} - override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {} }) } 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 74ae1932..c994ebab 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 @@ -2,10 +2,8 @@ package io.github.wulkanowy.utils.security -import android.annotation.TargetApi import android.content.Context import android.os.Build.VERSION.SDK_INT -import android.os.Build.VERSION_CODES.JELLY_BEAN_MR2 import android.os.Build.VERSION_CODES.M import android.security.KeyPairGeneratorSpec import android.security.keystore.KeyGenParameterSpec @@ -116,7 +114,6 @@ fun decrypt(cipherText: String): String { } } -@TargetApi(JELLY_BEAN_MR2) private fun generateKeyPair(context: Context) { (if (SDK_INT >= M) { KeyGenParameterSpec.Builder(KEY_ALIAS, PURPOSE_DECRYPT or PURPOSE_ENCRYPT) diff --git a/app/src/main/play/listings/cs-CZ/full-description.txt b/app/src/main/play/listings/cs-CZ/full-description.txt new file mode 100644 index 00000000..1420f5d6 --- /dev/null +++ b/app/src/main/play/listings/cs-CZ/full-description.txt @@ -0,0 +1,14 @@ +Aplikace je určena pro uživatele deníku VULCAN UONET+. + +Zvýrazněné vlastnosti a funkce: +- výpočet váženého průměru, +- procentuální zobrazení docházky, +- šťastné číslo, +- náhled na další a dokončené lekce, +- tmavý motiv, +- žádné reklamy, +- offline režim, +- upozornění. + +GitHub: https://github.com/wulkanowy/wulkanowy +Discord: https://discord.gg/vccAQBr diff --git a/app/src/main/play/listings/cs-CZ/short-description.txt b/app/src/main/play/listings/cs-CZ/short-description.txt new file mode 100644 index 00000000..0f29ab1b --- /dev/null +++ b/app/src/main/play/listings/cs-CZ/short-description.txt @@ -0,0 +1 @@ +Neoficiální aplikace žáka a rodiče pro deníku VULCAN UONET+ diff --git a/app/src/main/play/listings/cs-CZ/title.txt b/app/src/main/play/listings/cs-CZ/title.txt new file mode 100644 index 00000000..b7f42a5b --- /dev/null +++ b/app/src/main/play/listings/cs-CZ/title.txt @@ -0,0 +1 @@ +Wulkanowy Deníček diff --git a/app/src/main/play/listings/sk/full-description.txt b/app/src/main/play/listings/sk/full-description.txt new file mode 100644 index 00000000..2a4787d2 --- /dev/null +++ b/app/src/main/play/listings/sk/full-description.txt @@ -0,0 +1,14 @@ +Aplikácia je určená pre užívateľov denníka VULCAN UONET+. + +Zvýraznené vlastnosti a funkcie: +- výpočet váženého priemeru, +- percentuálne zobrazenie dochádzky, +- šťastné číslo, +- náhľad na ďalšie a dokončené lekcie, +- tmavý motív, +- žiadne reklamy, +- offline režim, +- upozornenia. + +GitHub: https://github.com/wulkanowy/wulkanowy +Discord: https://discord.gg/vccAQBr diff --git a/app/src/main/play/listings/sk/short-description.txt b/app/src/main/play/listings/sk/short-description.txt new file mode 100644 index 00000000..645ebbb6 --- /dev/null +++ b/app/src/main/play/listings/sk/short-description.txt @@ -0,0 +1 @@ +Neoficiálna aplikácia žiaka a rodiča pre denníka VULCAN UONET+ diff --git a/app/src/main/play/listings/sk/title.txt b/app/src/main/play/listings/sk/title.txt new file mode 100644 index 00000000..aa50ce77 --- /dev/null +++ b/app/src/main/play/listings/sk/title.txt @@ -0,0 +1 @@ +Wulkanowy Denníček 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 aeef2a77..5c809cfa 100644 --- a/app/src/main/play/release-notes/pl-PL/default.txt +++ b/app/src/main/play/release-notes/pl-PL/default.txt @@ -1,7 +1,10 @@ -Wersja 1.2.1 +Wersja 1.4.0 -- dodaliśmy brakujące okienka z podglądem szczegółów ogłoszeń szkolnych -- naprawiliśmy rzucające się w oczy błędy na ekranie startowym -- naprawiliśmy też inne drobne błędy w wyglądzie i stabilności aplikacji +- dodaliśmy możliwość dodawania własnych zadań domowych +- dodaliśmy kafelek z wiadomościami od twórców +- dodaliśmy dodatkowy tryb rozwijania szczegółów ocen +- dodaliśmy wsparcie dla Androida 12 +- ulepszyliśmy powiadomienia na Mi Bandzie +- dokonaliśmy też kilka innych zmian i kosmetycznych poprawek poprawiających komfort używania aplikacji Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases diff --git a/app/src/main/res/drawable-v23/compat_splash_screen_no_icon_background.xml b/app/src/main/res/drawable-v23/compat_splash_screen_no_icon_background.xml new file mode 100644 index 00000000..dad56a17 --- /dev/null +++ b/app/src/main/res/drawable-v23/compat_splash_screen_no_icon_background.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable-v23/img_splash_logo.png b/app/src/main/res/drawable-v23/img_splash_logo.png deleted file mode 100644 index 61489d81..00000000 Binary files a/app/src/main/res/drawable-v23/img_splash_logo.png and /dev/null differ diff --git a/app/src/main/res/drawable-v23/layer_splash_background.xml b/app/src/main/res/drawable-v23/layer_splash_background.xml deleted file mode 100644 index 1b4b64ec..00000000 --- a/app/src/main/res/drawable-v23/layer_splash_background.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - diff --git a/app/src/main/res/drawable/background_luckynumber_widget.xml b/app/src/main/res/drawable/background_luckynumber_widget.xml index f29744d0..367c5527 100644 --- a/app/src/main/res/drawable/background_luckynumber_widget.xml +++ b/app/src/main/res/drawable/background_luckynumber_widget.xml @@ -1,6 +1,6 @@ - - + + diff --git a/app/src/main/res/drawable/background_luckynumber_widget_dark.xml b/app/src/main/res/drawable/background_luckynumber_widget_dark.xml index fa15fd85..cb094b57 100644 --- a/app/src/main/res/drawable/background_luckynumber_widget_dark.xml +++ b/app/src/main/res/drawable/background_luckynumber_widget_dark.xml @@ -1,6 +1,6 @@ - - + + diff --git a/app/src/main/res/drawable/ic_dashboard_warning.xml b/app/src/main/res/drawable/ic_dashboard_warning.xml new file mode 100644 index 00000000..e7a5dc5a --- /dev/null +++ b/app/src/main/res/drawable/ic_dashboard_warning.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_help.xml b/app/src/main/res/drawable/ic_help.xml new file mode 100644 index 00000000..9c6ba292 --- /dev/null +++ b/app/src/main/res/drawable/ic_help.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_settings_ads.xml b/app/src/main/res/drawable/ic_settings_ads.xml new file mode 100644 index 00000000..c333ea76 --- /dev/null +++ b/app/src/main/res/drawable/ic_settings_ads.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_splash_logo.xml b/app/src/main/res/drawable/ic_splash_logo.xml new file mode 100644 index 00000000..e2e74731 --- /dev/null +++ b/app/src/main/res/drawable/ic_splash_logo.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/img_splash_logo.png b/app/src/main/res/drawable/img_splash_logo.png deleted file mode 100644 index fb521bf6..00000000 Binary files a/app/src/main/res/drawable/img_splash_logo.png and /dev/null differ diff --git a/app/src/main/res/drawable/layer_splash_background.xml b/app/src/main/res/drawable/layer_splash_background.xml deleted file mode 100644 index 2cf46d1d..00000000 --- a/app/src/main/res/drawable/layer_splash_background.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml index e55ea8b9..1d5b5280 100644 --- a/app/src/main/res/layout/activity_login.xml +++ b/app/src/main/res/layout/activity_login.xml @@ -10,7 +10,7 @@ android:layout_height="wrap_content" android:background="@android:color/transparent" /> - diff --git a/app/src/main/res/layout/dialog_conference.xml b/app/src/main/res/layout/dialog_conference.xml new file mode 100644 index 00000000..d08edf4f --- /dev/null +++ b/app/src/main/res/layout/dialog_conference.xml @@ -0,0 +1,211 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_error.xml b/app/src/main/res/layout/dialog_error.xml index a78790bc..b52c99ac 100644 --- a/app/src/main/res/layout/dialog_error.xml +++ b/app/src/main/res/layout/dialog_error.xml @@ -4,7 +4,8 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:minWidth="300dp" - android:orientation="vertical"> + android:orientation="vertical" + tools:context=".ui.base.ErrorDialog"> + + diff --git a/app/src/main/res/layout/dialog_homework.xml b/app/src/main/res/layout/dialog_homework.xml index 22a03cb2..341cec54 100644 --- a/app/src/main/res/layout/dialog_homework.xml +++ b/app/src/main/res/layout/dialog_homework.xml @@ -4,6 +4,7 @@ android:id="@+id/parent" android:layout_width="match_parent" android:layout_height="match_parent" + android:minWidth="300dp" android:orientation="vertical"> + android:layout_height="1dp" + android:background="@drawable/ic_all_divider" /> @@ -52,6 +53,9 @@ style="@style/Widget.MaterialComponents.Button.TextButton.Dialog" android:layout_width="wrap_content" android:layout_height="36dp" + android:layout_alignParentEnd="true" + android:layout_alignParentBottom="true" + android:layout_gravity="center_vertical" android:layout_marginTop="8dp" android:layout_marginEnd="8dp" android:layout_marginBottom="8dp" @@ -60,9 +64,6 @@ android:insetRight="0dp" android:insetBottom="0dp" android:minWidth="88dp" - android:layout_gravity="center_vertical" - android:layout_alignParentEnd="true" - android:layout_alignParentBottom="true" 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 new file mode 100644 index 00000000..b9b8d1a2 --- /dev/null +++ b/app/src/main/res/layout/dialog_homework_add.xml @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_school_announcement.xml b/app/src/main/res/layout/dialog_school_announcement.xml index cbce4e02..96c11d4a 100644 --- a/app/src/main/res/layout/dialog_school_announcement.xml +++ b/app/src/main/res/layout/dialog_school_announcement.xml @@ -118,6 +118,7 @@ android:layout_marginEnd="24dp" android:paddingStart="0dp" android:paddingEnd="16dp" + tools:maxLines="5" android:text="@string/all_no_data" android:textIsSelectable="true" android:textSize="16sp" diff --git a/app/src/main/res/layout/fragment_attendance.xml b/app/src/main/res/layout/fragment_attendance.xml index 8016081b..4996b85d 100644 --- a/app/src/main/res/layout/fragment_attendance.xml +++ b/app/src/main/res/layout/fragment_attendance.xml @@ -1,14 +1,15 @@ - + android:layout_height="0dp" + android:layout_weight="1"> + app:srcCompat="@drawable/ic_chevron_left" + app:tint="?colorPrimary" /> + app:srcCompat="@drawable/ic_chevron_right" + app:tint="?colorPrimary" /> - + diff --git a/app/src/main/res/layout/fragment_exam.xml b/app/src/main/res/layout/fragment_exam.xml index ca88849c..0c62aab5 100644 --- a/app/src/main/res/layout/fragment_exam.xml +++ b/app/src/main/res/layout/fragment_exam.xml @@ -128,8 +128,8 @@ android:paddingRight="12dp" android:paddingBottom="8dp" android:scaleType="fitStart" - android:tint="?colorPrimary" - app:srcCompat="@drawable/ic_chevron_left" /> + app:srcCompat="@drawable/ic_chevron_left" + app:tint="?colorPrimary" /> + app:srcCompat="@drawable/ic_chevron_right" + app:tint="?colorPrimary" /> diff --git a/app/src/main/res/layout/fragment_grade.xml b/app/src/main/res/layout/fragment_grade.xml index ed0447fb..989929d4 100644 --- a/app/src/main/res/layout/fragment_grade.xml +++ b/app/src/main/res/layout/fragment_grade.xml @@ -17,7 +17,7 @@ tools:ignore="UnusedAttribute" tools:visibility="visible" /> - @@ -105,6 +107,18 @@ android:text="@string/all_retry" /> + + + app:srcCompat="@drawable/ic_chevron_left" + app:tint="?colorPrimary" /> + app:srcCompat="@drawable/ic_chevron_right" + app:tint="?colorPrimary" /> diff --git a/app/src/main/res/layout/fragment_login_form.xml b/app/src/main/res/layout/fragment_login_form.xml index 06d1fa5e..d1c997ff 100644 --- a/app/src/main/res/layout/fragment_login_form.xml +++ b/app/src/main/res/layout/fragment_login_form.xml @@ -110,10 +110,8 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginStart="32dp" - android:layout_marginLeft="32dp" android:layout_marginTop="32dp" android:layout_marginEnd="32dp" - android:layout_marginRight="32dp" android:gravity="center_horizontal" android:text="@string/login_header_default" android:textSize="16sp" @@ -126,6 +124,20 @@ app:layout_constraintVertical_chainStyle="packed" app:layout_goneMarginTop="64dp" /> + + app:layout_constraintTop_toBottomOf="@+id/loginFormErrorBox" + app:layout_goneMarginTop="48dp"> @@ -217,7 +227,6 @@ android:layout_marginRight="24dp" android:hint="@string/login_host_hint" android:orientation="vertical" - app:layout_constraintBottom_toTopOf="@+id/loginFormAdvancedButton" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/loginFormRecoverLink"> @@ -262,14 +271,13 @@ android:id="@+id/loginFormPrivacyLink" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginTop="24dp" android:gravity="start|center_vertical" android:text="@string/login_privacy_policy" android:textColor="?android:textColorSecondary" android:textSize="12sp" app:fontFamily="sans-serif-medium" app:layout_constraintStart_toStartOf="@id/loginFormAdvancedButton" - app:layout_constraintTop_toBottomOf="@+id/loginFormAdvancedButton" + app:layout_constraintTop_toTopOf="@+id/loginFormVersion" tools:visibility="visible" /> + app:srcCompat="@drawable/ic_chevron_left" + app:tint="?colorPrimary" /> + app:srcCompat="@drawable/ic_chevron_right" + app:tint="?colorPrimary" /> diff --git a/app/src/main/res/layout/fragment_message.xml b/app/src/main/res/layout/fragment_message.xml index a61f3738..5269d95e 100644 --- a/app/src/main/res/layout/fragment_message.xml +++ b/app/src/main/res/layout/fragment_message.xml @@ -5,7 +5,7 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - - diff --git a/app/src/main/res/layout/fragment_notifications_center.xml b/app/src/main/res/layout/fragment_notifications_center.xml new file mode 100644 index 00000000..f59ce33c --- /dev/null +++ b/app/src/main/res/layout/fragment_notifications_center.xml @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_school.xml b/app/src/main/res/layout/fragment_school.xml index 32be61db..3f5dc6e2 100644 --- a/app/src/main/res/layout/fragment_school.xml +++ b/app/src/main/res/layout/fragment_school.xml @@ -15,19 +15,18 @@ + android:layout_height="match_parent"> + android:orientation="vertical" + android:paddingVertical="8dp" + android:paddingStart="8dp" + android:paddingEnd="12dp" + android:visibility="invisible" + tools:visibility="visible"> + + android:background="?attr/selectableItemBackgroundBorderless" + android:contentDescription="@string/school_address_button" + android:padding="4dp" + app:srcCompat="@drawable/ic_school_directions" + app:tint="?colorPrimary" /> + + android:background="?attr/selectableItemBackgroundBorderless" + android:contentDescription="@string/school_telephone_button" + android:padding="4dp" + app:srcCompat="@drawable/ic_all_phone" + app:tint="?colorPrimary" /> - - + app:srcCompat="@drawable/ic_chevron_left" + app:tint="?colorPrimary" /> + app:srcCompat="@drawable/ic_chevron_right" + app:tint="?colorPrimary" /> diff --git a/app/src/main/res/layout/fragment_timetable_additional.xml b/app/src/main/res/layout/fragment_timetable_additional.xml index 61eb4445..a71f7545 100644 --- a/app/src/main/res/layout/fragment_timetable_additional.xml +++ b/app/src/main/res/layout/fragment_timetable_additional.xml @@ -131,8 +131,8 @@ android:paddingRight="12dp" android:paddingBottom="8dp" android:scaleType="fitStart" - android:tint="?colorPrimary" - app:srcCompat="@drawable/ic_chevron_left" /> + app:srcCompat="@drawable/ic_chevron_left" + app:tint="?colorPrimary" /> + app:srcCompat="@drawable/ic_chevron_right" + app:tint="?colorPrimary" /> diff --git a/app/src/main/res/layout/fragment_timetable_completed.xml b/app/src/main/res/layout/fragment_timetable_completed.xml index 1a890fe1..e089275d 100644 --- a/app/src/main/res/layout/fragment_timetable_completed.xml +++ b/app/src/main/res/layout/fragment_timetable_completed.xml @@ -130,8 +130,8 @@ android:paddingRight="12dp" android:paddingBottom="8dp" android:scaleType="fitStart" - android:tint="?colorPrimary" - app:srcCompat="@drawable/ic_chevron_left" /> + app:srcCompat="@drawable/ic_chevron_left" + app:tint="?colorPrimary" /> + app:srcCompat="@drawable/ic_chevron_right" + app:tint="?colorPrimary" /> diff --git a/app/src/main/res/layout/item_dashboard_admin_message.xml b/app/src/main/res/layout/item_dashboard_admin_message.xml new file mode 100644 index 00000000..265cc14e --- /dev/null +++ b/app/src/main/res/layout/item_dashboard_admin_message.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_dashboard_lessons.xml b/app/src/main/res/layout/item_dashboard_lessons.xml index a2a92c54..9156c1a2 100644 --- a/app/src/main/res/layout/item_dashboard_lessons.xml +++ b/app/src/main/res/layout/item_dashboard_lessons.xml @@ -42,6 +42,19 @@ app:layout_constraintBottom_toBottomOf="@id/dashboard_lessons_item_title" app:layout_constraintStart_toEndOf="@id/dashboard_lessons_item_title" /> + + + android:orientation="horizontal"> + + - diff --git a/app/src/main/res/layout/item_notifications_center.xml b/app/src/main/res/layout/item_notifications_center.xml new file mode 100644 index 00000000..16a7ae0c --- /dev/null +++ b/app/src/main/res/layout/item_notifications_center.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/scrollable_header_about.xml b/app/src/main/res/layout/scrollable_header_about.xml index e203d98d..5a7669fd 100644 --- a/app/src/main/res/layout/scrollable_header_about.xml +++ b/app/src/main/res/layout/scrollable_header_about.xml @@ -6,6 +6,7 @@ android:layout_height="wrap_content" android:minHeight="104dp" android:orientation="vertical" + android:paddingHorizontal="20dp" tools:context=".ui.modules.about.AboutAdapter"> + app:layout_constraintTop_toTopOf="@id/aboutScrollableHeaderIcon" + app:layout_constraintWidth_max="wrap" /> diff --git a/app/src/main/res/layout/scrollable_header_grade_summary.xml b/app/src/main/res/layout/scrollable_header_grade_summary.xml index 29657ba1..049219a9 100644 --- a/app/src/main/res/layout/scrollable_header_grade_summary.xml +++ b/app/src/main/res/layout/scrollable_header_grade_summary.xml @@ -1,5 +1,6 @@ - + android:layout_gravity="center" + android:orientation="horizontal"> + + + + + - + android:layout_gravity="center" + android:orientation="horizontal"> + + + + + + android:layout_height="wrap_content" + xmlns:tools="http://schemas.android.com/tools"> + tools:text="Urządzenia techniki komputerowych" /> diff --git a/app/src/main/res/menu/action_menu_dashboard.xml b/app/src/main/res/menu/action_menu_dashboard.xml index dbdd6e81..13565a19 100644 --- a/app/src/main/res/menu/action_menu_dashboard.xml +++ b/app/src/main/res/menu/action_menu_dashboard.xml @@ -1,10 +1,17 @@ + diff --git a/app/src/main/res/menu/action_menu_main.xml b/app/src/main/res/menu/action_menu_main.xml index 21905939..f14d1f74 100644 --- a/app/src/main/res/menu/action_menu_main.xml +++ b/app/src/main/res/menu/action_menu_main.xml @@ -5,7 +5,7 @@ diff --git a/app/src/main/res/values-cs/preferences_values.xml b/app/src/main/res/values-cs/preferences_values.xml index fb938f09..5252f79b 100644 --- a/app/src/main/res/values-cs/preferences_values.xml +++ b/app/src/main/res/values-cs/preferences_values.xml @@ -40,15 +40,20 @@ Wulkanowy Barvy známek v deníku + + Až 1 najednou + Vždy rozbalené + Neomezené rozbalené + - Průměrná známka od druhého semestru - Průměr známek z obou semestrů + Průměr známek pouze z vybraného semestru + Průměr z průměrů z obou semestrů Průměr známek z celého roku Šťastné číslo Nepřečtené zprávy - Docházka + Frekvence Lekce Známky Domácí úkoly diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index fedbc903..687962e4 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -4,7 +4,7 @@ Přihlášení Wulkanowy Známky - Docházka + Frekvence Zkoušky Plán lekce Nastavení @@ -12,11 +12,12 @@ O aplikaci Prohlížeč protokolů Ladění - Ladění oznámení + Ladění upozornění Tvůrci Licence Zprávy Nová zpráva + Nový domácí úkol Poznámky a úspěchy Domácí úkoly Manažer účtů @@ -24,11 +25,12 @@ Podrobnosti účtu Informace o žáku Domů + Centrum upozornění Semestr %1$d, %2$d/%3$d Přihlaste se pomocí studentského nebo rodičovského účtu - Zadejte symbol ze stránky deníku + Zadejte symbol ze stránky deníku: <b>%1$s</b> Uživatelské jméno Email Přihlášení, číslo PESEL nebo e-mail @@ -42,7 +44,8 @@ Symbol Přihlásit Toto heslo je příliš krátké - Přihlašovací údaje jsou nesprávné. Ujistěte se, že je v poli níže vybrána správná variace deníku UONET+ + Přihlašovací údaje jsou nesprávné + %1$s. Zkontrolujte, zda je níže vybrána správná variace deníku UONET+ Neplatný PIN Neplatný token Token vypršel @@ -51,15 +54,14 @@ Použijte přiřazené přihlašovací nebo e-mail v @%1$s Neplatný symbol Žák nebyl nalezen. Zkontrolujte správnost symbolu a vybrané varianty deníku UONET+ - Toto pole je povinné Vybraný žák je už přihlášen Symbol najdete na stránce deníku v  Uczeń → Dostęp Mobilny → Zarejestruj urządzenie mobilne.\n\nUjistěte se, že jste na předchozí obrazovce nastavili správnou variantu deníku do pole Variace deníku UONET+. Wulkanowy v tuto chvíli nezjistí předškolní żaków Vyberte žáky, kteří se mají do aplikace přihlásit Jiné možnosti - V tomto režimu nefungují následující: šťastné číslo, statistiky třídy, shrnutí docházky, ospravedlnění nepřítomnosti, dokončené lekce, informace o škole a prohlížení seznamu registrovaných zařízení + V tomto režimu nefungují následující: šťastné číslo, statistiky třídy, shrnutí frekvencí, ospravedlnění nepřítomnosti, dokončené lekce, informace o škole a prohlížení seznamu registrovaných zařízení Tento režim zobrazuje stejná data, která se zobrazují na webových stránkách deníka Kombinace nejlepších vlastností ostatních dvou režimů. Funguje rychleji než scraper a poskytuje funkce, které nejsou k dispozici v režimu Mobile API. Je to v experimentální fázi - Zásady ochrany osobních údajů + Ochrana osobních údajů Problémy s přihlášením? Napište nám! Email Discord @@ -90,6 +92,10 @@ Konečná známka Předpokládaná 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 souč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ý.\n3. Sčítání vypočítaných průměrů\n4. Výpočet aritmetického průměru součtených průměrů + Jak funguje konečný průměr? + Konečný průměr je aritmetický průměr vypočítaný ze všech aktuálně dostupných konečných známek v daném semestru.\n\nSchéma výpočtu se skládá z následujících kroků:\n1. Sčítání konečných známek zadaných učiteli\n2. Děleno počtem předmětů, pro které už byly vydány známky Konečný průměr z %1$d z %2$d předmětů Shrnutí @@ -159,6 +165,34 @@ Teď: %s Za chvíli: %s Později: %s + %1$s lekce %2$d - %3$s + Změna učebny z %1$s na %2$s + Změna učitele z %1$s na %2$s + Změna předmětu z %1$s na %2$s + + Změna plánu lekcí + Změny plánu lekcí + Změny plánu lekcí + Změny plánu lekcí + + + %1$s - %2$d změna plánu lekcí + %1$s - %2$d změny plánu lekcí + %1$s - %2$d změn plánu lekcí + %1$s - %2$d změn plánu lekcí + + + %1$d změna plánu lekcí + %1$d změny plánu lekcí + %1$d změn plánu lekcí + %1$d změn plánu lekcí + + + %d změna + %d změny + %d změn + %d změn + Dokončené lekce Zobrazit dokončené lekce @@ -171,7 +205,7 @@ Zobrazit další lekce Žádné informace o dalších lekcích - Shrnutí docházky + Shrnutí frekvencí Neprítomnosť zo školských dôvodov Omluvená nepřítomnost Neomluvená nepřítomnost @@ -188,6 +222,24 @@ Žádost o omluvu nepřítomnosti byla úspěšně odeslána! Musíte vybrat alespoň jednu nepřítomnost! Ospravedlnit + + Nové frekvence + Nové frekvence + Nové frekvence + Nové frekvence + + + %1$d nové frekvence + %1$d nové frekvence + %1$d nových frekvencí + %1$d nových frekvencí + + + %d frekvence + %d frekvence + %d frekvencí + %d frekvencí + Společně @@ -238,12 +290,7 @@ Pouze nepřečtené Pouze s přílohami Přečtena: %s - - Přečtena přes: %1$d z %2$d osob - Přečtena přes: %1$d z %2$d osob - Přečtena přes: %1$d z %2$d osob - Přečtena přes: %1$d z %2$d osob - + Přečtena přes: %1$d z %2$d osob %d zpráva %d zprávy @@ -327,6 +374,9 @@ Žádné informace o domácích úkolech Označit jako hotové Neudělané + Přidat domácí úkol + Domácí úkol byl úspěšně přidán + Domácí úkol byl úspěšně odstraněn Přílohy Nový domácí úkol @@ -402,6 +452,8 @@ Máte %1$d nových setkání Máte %1$d nových setkání + Přítomnost na setkání + Agenda Školní oznámení Žádná školní oznámení @@ -447,11 +499,11 @@ Přečtěte si často kladené otázky Server Discord Připojte se ke komunitě Wulkanového - Facebooková fanpage + Stránka na Facebooku Twitter stránka Sledujte nás na Twitteru - Stejně jako naše facebooková fanpage - Zásady ochrany osobních údajů + Dejte like naší stránce na Facebooku + Ochrana osobních údajů Pravidla pro shromažďování osobních údajů Systemová nastavení Otevřít systémová nastavení @@ -496,6 +548,7 @@ Lekce (Zítra) + (Dnes a zítra) Za chvíli: Brzy: První: @@ -516,10 +569,10 @@ Další: Později: - ještě %1$d další lekce - ještě %1$d další lekce - ještě %1$d dalších lekcí - ještě %1$d dalších lekcí + ještě %1$d lekce + ještě %1$d lekce + ještě %1$d lekcí + ještě %1$d lekcí do %1$s Žádné nadcházející lekce @@ -528,10 +581,10 @@ Žádné domácí úkoly do vykonána Při načítání domácích úkolů došlo k chybě - Ještě %1$d další domácí úkol - Ještě %1$d další domácí úkoly - Ještě %1$d dalších domácích úkolů - Ještě %1$d dalších domácích úkolů + Ještě %1$d domácí úkol + Ještě %1$d domácí úkoly + Ještě %1$d domácích úkolů + Ještě %1$d domácích úkolů do %1$s Poslední známky @@ -541,28 +594,28 @@ Žádná aktuální oznámení Při načítání oznámení došlo k chybě - Ještě %1$d další oznámení - Ještě %1$d další oznámení - Ještě %1$d dalších oznámení - Ještě %1$d dalších oznámení + Ještě %1$d oznámení + Ještě %1$d oznámení + Ještě %1$d oznámení + Ještě %1$d oznámení Zkoušky Žádné nadcházející zkoušky Při načítání zkoušek došlo k chybě - Ještě %1$d další zkouška - Ještě %1$d další zkoušky - Ještě %1$d dalších zkoušek - Ještě %1$d dalších zkoušek + Ještě %1$d zkouška + Ještě %1$d zkoušky + Ještě %1$d zkoušek + Ještě %1$d zkoušek Setkání Žádná nadcházející setkání Při načítání setkání došlo k chybě - Ještě %1$d další setkání - Ještě %1$d další setkání - Ještě %1$d dalších setkání - Ještě %1$d dalších setkání + Ještě %1$d setkání + Ještě %1$d setkání + Ještě %1$d setkání + Ještě %1$d setkání Při načítání dat došlo k chybě Žádné @@ -590,6 +643,11 @@ Ano Ne Uložit + Titul + Přidat + Zkopírováno + Vrátit + Změnit Žádné lekce Vybrat motiv @@ -597,13 +655,13 @@ Tmavý Motiv systému - Vzhled a chování aplikací + Aplikace Výchozí zobrazení - Výpočet koncoročního průměru + Možnosti vypočítaného průměru Vynutit průměrný výpočet podle aplikace Zobrazit přítomnost Motiv - Rozbalit známky + Rozvíjení známek Označit aktuální lekci Zobrazit skupiny vedle předmětů Zobrazit seznam grafů v známkách třídy @@ -612,14 +670,21 @@ Třídění předmětů Jazyk Upozornění + Jiné Zobrazit upozornění Zobrazit upozornění o nadcházející lekci + Nastavit upozornění o nadcházející lekci jako trvalé + Vypnout, když upozornění není ve vašem hodinkách/náramku viditelné Otevřít systémová nastavení upozornění Opravte problémy se synchronizací a upozorněním Vaše zařízení může mít problémy se synchronizací dat as upozorněními.\n\nChcete-li je opravit, přidejte Wulkanového do funkce Autostart a vypněte optimalizaci/úsporu baterie v nastavení systému telefonu. Přejít do nastavení Zobrazit upozornění o ladění Synchronizace je vypnutá + Zachytit upozornění oficiální aplikací + Zachytit upozornění + S touto funkcí můžete získat náhradu push upozornění jako v oficiální aplikaci. Vše, co musíte udělat, je povolit Wulkanowému číst všechna vaše upozornění v nastaveních systému.\n\nJak to funguje?\nKdyž obdržíte oznámení v Deníčku VULCAN, Wulkanowy bude o tom informován (k tomu je to dodatečné povolení) a spustí synchronizaci, aby mohl zaslat vlastní upozornění.\n\nPOUZE PRO POKROČILÉ UŽIVATELE + Přejít do nastavení Synchronizace Automatická aktualizace Pozastaveno na dovolené @@ -634,16 +699,25 @@ Hodnota mínusu Odpovědět s historií zpráv Vypočítat aritmetický průměr, pokud žádná známka nemá váhu + Podpora + Podívejte se na jednu reklamu pro podporu projektu + Souhlas se zpracováním dat + Jestli chcete sledovat reklamu, musíte souhlasit s podmínkami zpracování údajů v našich Zásadách Ochrany Osobních Údajů + Souhlasím + Ochrana osobních údajů + Reklama se načítá Pokročilé Vzhled a chování Upozornění Synchronizace + Reklamy Známky Domů Viditelnost dlaždic - Docházka + Frekvence Plán lekce Známky + Vypočítaný průměr Zprávy Vzhled a chování Jazyky, motivy, třídění předmětů @@ -653,7 +727,8 @@ Automatická aktualizace, interval aktualizací Hodnota plusu a mínusu, výpočet průměru Pokročilé - Verze aplikace, tvůrci, sociální portály, licence + Verze aplikace, tvůrci, sociální portály + Zobrazování reklam, podpora projektu Nové známky Nové domácí úkoly @@ -666,6 +741,8 @@ Push upozornění Nadcházející lekce Ladění + Změny plánu lekcí + Nové frekvence Černá Červená @@ -673,10 +750,6 @@ Zelená Fialová Žádná barva - - Zkopírováno - Vrátit - Změnit Stahování aktualizací začalo… Aktualizace byla stažena. @@ -693,4 +766,5 @@ 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 + Toto pole je povinné diff --git a/app/src/main/res/values-de/preferences_values.xml b/app/src/main/res/values-de/preferences_values.xml index 1e0df8de..23e54cb7 100644 --- a/app/src/main/res/values-de/preferences_values.xml +++ b/app/src/main/res/values-de/preferences_values.xml @@ -40,9 +40,14 @@ Wulkanowy Farben der Bewertungen im Logbuch + + Up to 1 at once + Always expanded + Unlimited expansions + - Durchschnittsnote für das 2. Semester Durchschnitt der Noten aus beiden Semestern + Durchschnittswert der Durchschnittswerte beider Semester Durchschnitt der Noten aus dem ganzen Jahr diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 5f14a425..9ad6e372 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -6,7 +6,7 @@ Noten Schulbesuch Prüfungen - Zeitplan + Stundenplan Einstellungen Mehr Über die Applikation @@ -17,6 +17,7 @@ Lizenzen Nachrichten neue Nachricht + New homework Eintragen und Erfolgen Hausaufgaben Konten-Manager @@ -24,11 +25,12 @@ Kontodetails Schülerinfo Übersicht + Benachrichtigungszentrum Semester %1$d, %2$d/%3$d Melden Sie sich mit dem Studenten- oder Elternkonto an - Geben Sie das Symbol von der Registerseite ein + Geben Sie das Symbol von der Registerseite ein: <b>%1$s</b> Benutzername Email Anmeldung, PESEL oder e-mail @@ -42,7 +44,8 @@ Symbol Anmelden Passwort ist zu kurz - Anmeldedaten sind falsch. Stellen Sie sicher, dass die richtige UONET+ Registervariation im unteren Feld ausgewählt ist + Anmeldedaten sind falsch + %1$s. Stellen Sie sicher, dass die korrekte UONET+ Registervariation unten ausgewählt ist Ungültige PIN Ungültige token Token ist nicht mehr gültig @@ -51,7 +54,6 @@ Benutze den zugewiesenen Login oder E-Mail in @%1$s Ungültige symbol Schüler nicht gefunden. Überprüfen Sie das Symbol und die gewählte Variation des UONET+ Registers - Dieses Datenfeld ist erforderlich Ausgewählter Student ist bereits angemeldet. Das Symbol kann auf der Registerseite in Uczeń→ Dostęp Mobilny → Zarejestruj urządzenie mobilnegefunden werden.\n\nStellen Sie sicher, dass Sie die entsprechende Registervariante im Feld UONET+ Registervariante auf dem vorherigen Bildschirm festgelegt haben. Wulkanowy erkennt zur Zeit keine Vorschulstudenten Wählen Sie die Studenten aus, die sich bei der Anwendung anmelden sollen. @@ -90,6 +92,10 @@ Finaler Note Vorhergesagte Note Berechnender Durchschnitt + Wie funktioniert der berechnete Durchschnitt? + 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.\n3. Adding calculated averages\n4. Calculating the arithmetic average of summed averages + Wie funktioniert der endgültige Durchschnitt? + The Final Average is the arithmetic average calculated from all currently available final grades in the given semester.\n\nThe calculation scheme consists of the following steps:\n1. Summing up the final grades given by teachers\n2. Divide by the number of subjects that have already been graded Finaler Durchschnitt aus %1$d von %2$d Schulfächern Zusammenfassung @@ -145,6 +151,26 @@ Jetzt: %s In einem Moment: %s Später: %s + %1$s lesson %2$d - %3$s + Change of room from %1$s to %2$s + Change of teacher from %1$s to %2$s + Change of subject from %1$s to %2$s + + Timetable change + Timetable changes + + + %1$s - %2$d change in timetable + %1$s - %2$d changes in timetable + + + %1$d change in timetable + %1$d changes in timetable + + + %d change + %d changes + Beendete Lektionen Beendete Lektionen anzeigen @@ -155,7 +181,7 @@ Zusätzliche Lektionen Zusätzliche Lektionen anzeigen - Keine Infos zu zusätzlichen Lektionen + Keine Informationen über zusätzlichen Lektionen Übersicht über die Schulbesuch Aus schulischen Gründen abwesend @@ -174,6 +200,18 @@ Abwesenheitsentschuldigungsanfrage erfolgreich gesendet! Sie müssen mindestens eine Abwesenheit auswählen! Verzeihung + + New attendance + New attendance + + + %1$d new attendance + %1$d attendance + + + %d attendance + %d attendance + Gesamt @@ -185,8 +223,8 @@ Neue prüfungen - Du hast %d neue Prüfung erhalten - Sie haben %d neue Prüfungen erhalten + Du hast %d neue Prüfung + Du hast %d neue Prüfungen %d prüfung @@ -212,16 +250,13 @@ Thema Inhalt Nachricht erfolgreich gesendet - Nachricht existiert nicht + Nachricht nicht vorhanden Sie müssen mindestens 1 Empfänger auswählen. Der Inhalt der Nachricht muss mindestens 3 Zeichen lang sein. Nur ungelesen Nur mit Anhängen Lesen: %s - - Lesen von: %1$d von %2$d Personen - Lesen von: %1$d von %2$d Personen - + Lesen von: %1$d von %2$d Personen %d nachricht %d nachrichten @@ -281,6 +316,9 @@ Keine Informationen über Hausaufgaben Gemacht Unvollständig + Add homework + Homework added successfully + Homework deleted successfully Anhänge Neue hausaufgaben @@ -344,6 +382,8 @@ Sie haben %1$d neue konferenz Sie haben %1$d neue konferenzen + Teilnahme an einem Meeting + Agenda Schulankündigungen Keine schulankündigungen @@ -432,6 +472,7 @@ Lektionen (Morgen) + (Today and tomorrow) Gleich: Bald: Erstens: @@ -512,6 +553,11 @@ Ja Nein Speichern + Titel + Add + Kopiert + lösen + Ändern Keine Lektionen Thema wählen @@ -519,13 +565,13 @@ Dunkel Systemthema - Aussehen & Verhalten + App Standard Ansicht - Berechnung des Jahresenddurchschnitts + Berechnete Durchschnittsoptionen Mittelwertberechnung durch App erzwingen Anwesendheit zeigen Thema - Noten erweitern + Grades expanding Aktuelle Lektion markieren Gruppen neben Schulfächen anzeigen Liste der Diagramme in Klassenbewertungen anzeigen @@ -534,14 +580,21 @@ Schulfachen sortieren Sprache Benachrichtigungen + Other Benachrichtigungen anzeigen Benachrichtigungen über bevorstehende Lektionen anzeigen + Festlegen einer Benachrichtigung über die bevorstehende Lektion dauerhaft + Deaktivieren wenn die Benachrichtigung nicht in deiner Uhr/Band angezeigt wird Systembenachrichtigungseinstellungen öffnen Synchronisierungs- und Benachrichtigungsprobleme reparieren Ihr Gerät hat möglicherweise Probleme mit der Datensynchronisierung und Benachrichtigungen.\n\nUm diese zu reparieren, fügen Sie Wulkanowy zum Autostart hinzu und deaktivieren Sie die Batterieoptimierung in den Systemeinstellungen des Geräts. Gehe zu den Einstellungen Debug-Benachrichtigungen anzeigen Synchronisierung ist deaktiviert + Offizielle App-Benachrichtigungen erfassen + Benachrichtigungen erfassen + With this feature you can gain a substitute of push notifications like in the official app. All you need to do is allow Wulkanowy to receive all notifications in your system settings.\n\nHow it works?\nWhen you get a notification in Dziennik VULCAN, Wulkanowy will be notified (that\'s what these extra permissions are for) and will trigger a sync so that can send its own notification.\n\nFOR ADVANCED USERS ONLY + Gehe zu Einstellungen Synchronisierung Automatische Aktualisierung An Feiertagen suspendiert @@ -556,16 +609,25 @@ Wert des Minus Antwort mit Nachrichtenhistorie Arithmetisches Mittel anzeigen, wenn keine Gewichte angegeben sind + Support + Watch single ad to support project + Consent to data processing + To view an advertisement you must agree to the data processing terms of our Privacy Policy + Agree + Privacy policy + Ad is loading Erweitert Aussehen & Verhalten Benachrichtigungen Synchronisierung + Advertisements Noten Dashboard Sichtbarkeit der Kacheln Schulbesuch - Zeitplan + Stundenplan Noten + Calculated average Nachrichten Aussehen & Verhalten Sprachen, Themen, Schulfachen sortieren @@ -575,7 +637,8 @@ Automatisches Update, Synchronisierungsintervall Plus und Minus Werte, Durchschnittsberechnung Erweitert - App-Version, Mitarbeiter, soziale Portale, Lizenzen + App version, contributors, social portals + Displaying advertisements, project support Neue Noten Neue Hausaufgaben @@ -588,6 +651,8 @@ Push-Benachrichtigungen Bevorstehende Lektionen Debuggen + Timetable change + New attendance Schwarz Rot @@ -595,10 +660,6 @@ Grün Violett Keine Farbe - - Kopiert - lösen - Ändern Download der Updates wurde gestartet… Ein Update wurde gerade heruntergeladen. @@ -615,4 +676,5 @@ Ein unerwarteter Fehler ist aufgetreten Funktion, die von Ihrer Schule deaktiviert wurde Feature in diesem Modus nicht verfügbar + This field is required diff --git a/app/src/main/res/values-night/styles.xml b/app/src/main/res/values-night/styles.xml index 5eca4680..881d5bd4 100644 --- a/app/src/main/res/values-night/styles.xml +++ b/app/src/main/res/values-night/styles.xml @@ -8,6 +8,7 @@ @color/colorErrorLight @color/colorDividerInverse @color/colorSwipeRefreshDark + @color/dashboard_message_medium_light ?colorSurface ?android:textColorPrimary @color/colorNavigationBarLight diff --git a/app/src/main/res/values-pl/preferences_values.xml b/app/src/main/res/values-pl/preferences_values.xml index 12cfda8c..c823e960 100644 --- a/app/src/main/res/values-pl/preferences_values.xml +++ b/app/src/main/res/values-pl/preferences_values.xml @@ -40,9 +40,14 @@ Wulkanowy Kolory ocen w dzienniku + + Do 1 na raz + Zawsze rozwinięte + Nieograniczone rozwijanie + - Średnia ocen z drugiego semestru - Średnia średnich z obu semestrów + Średnia ocen tylko z wybranego semestru + Średnia ze średnich z obu semestrów Średnia wszystkich ocen z całego roku diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 14f23cd2..4a1337e0 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -17,6 +17,7 @@ Licencje Wiadomości Nowa wiadomość + Nowe zadanie domowe Uwagi i osiągnięcia Zadania domowe Menadżer kont @@ -24,14 +25,15 @@ Szczegóły konta Informacje o uczniu Start + Centrum powiadomień Semestr %1$d, %2$d/%3$d Zaloguj się za pomocą konta ucznia lub rodzica - Podaj symbol ze strony dziennika + Podaj symbol ze strony dziennika dla konta: <b>%1$s</b> Nazwa użytkownika - Email - Login, PESEL lub e-mail + Adres e-mail + Login, PESEL lub adres e-mail Hasło Odmiana dziennika UONET+ Mobilne API @@ -42,16 +44,16 @@ Symbol Zaloguj To hasło jest za krótkie - Dane logowania są niepoprawne. Upewnij się, że została wybrana odpowiednia odmiana dziennika UONET+ w polu poniżej + Dane logowania są niepoprawne + %1$s. Upewnij się, że wybrano poprawną odmianę dziennika UONET+ poniżej Nieprawidłowy PIN Nieprawidłowy token Token stracił ważność - Niepoprawny adres email - Użyj przydzielonego loginu zamiast emaila - Użyj przypisanego loginu lub adresu e-mail w @%1$s - Niepoprawny symbol + Nieprawidłowy adres e-mail + Użyj loginu zamiast adresu e-mail + Użyj loginu lub adresu e-mail w @%1$s + Nieprawidłowy symbol Nie znaleziono ucznia. Sprawdź poprawność symbolu i wybranej odmiany dziennika UONET+ - To pole jest wymagane Wybrany uczeń jest już zalogowany Symbol znajdziesz na stronie dziennika w Uczeń → Dostęp Mobilny → Zarejestruj urządzenie mobilne.\n\nUpewnij się, że w polu Dziennik UONET+ na poprzednim ekranie została ustawiona odpowiednia odmiana dziennika.\n\nWulkanowy na chwilę obecną nie wykrywa uczniów przedszkolnych (z zerówki) Wybierz uczniów do zalogowania w aplikacji @@ -61,9 +63,9 @@ Połączenie najlepszych cech dwóch pozostałych trybów. Działa szybciej niż scraper i zapewnia funkcje niedostępne w trybie Mobilne API. Jest w fazie eksperymentalnej Polityka prywatności Problemy z logowaniem? Napisz do nas! - Email + E-mail Discord - Wyślij email + Wyślij wiadomość e-mail Upewnij się, że została wybrana odpowiednia odmiana dziennika UONET+! Nie pamiętam hasła Przywróć swoje konto @@ -90,6 +92,10 @@ Ocena końcowa Przewidywana ocena 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 semetrze 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.\n3. Zsumowanie obliczonych średnich\n4. Obliczanie średniej arytmetycznej z zsumowanych średnich + Jak działa końcowa średnia? + Średnią końcową jest średnia arytmetyczna obliczona na podstawie wszystkich obecnie dostępnych ocen końcowych w danym semestrze.\n\nSchemat obliczeń składa się z następujących kroków:\n1. Sumowanie końcowych ocen wpisanych przez nauczycieli\n2. Dzielenie przez liczbę przedmiotów, z których oceny zostały już wystawione Końcowa średnia z %1$d na %2$d przedmiotów Podsumowanie @@ -159,6 +165,34 @@ Teraz: %s Za chwilę: %s Później: %s + %1$s lekcja %2$d - %3$s + Zmiana sali z %1$s na %2$s + Zmiana nauczyciela z %1$s na %2$s + Zmiana przedmiotu z %1$s na %2$s + + Zmiana planu lekcji + Zmiany planu lekcji + Zmiany planu lekcji + Zmiany planu lekcji + + + %1$s - %2$d zmiana planu lekcji + %1$s - %2$d zmiany planu lekcji + %1$s - %2$d zmian planu lekcji + %1$s - %2$d zmian planu lekcji + + + %1$d zmiana planu lekcji + %1$d zmiany planu lekcji + %1$d zmian planu lekcji + %1$d zmian planu lekcji + + + %d zmiana + %d zmiany + %d zmian + %d zmian + Lekcje zrealizowane Zobacz lekcje zrealizowane @@ -188,6 +222,24 @@ Prośba o usprawiedliwienie została pomyślnie wysłana! Musisz wybrać co najmniej jedną nieobecność! Usprawiedliw + + Nowa frekwencja + Nowe frekwencje + Nowe frekwencje + Nowe frekwencje + + + %1$d nowa frekwencja + %1$d nowe frekwencje + %1$d nowych frekwencji + %1$d nowych frekwencji + + + %d frekwencja + %d frekwencje + %d frekwencji + %d frekwencji + Razem @@ -238,12 +290,7 @@ Tylko nieprzeczytane Tylko z załącznikami Przeczytana: %s - - Przeczytana przez: %1$d z %2$d osób - Przeczytana przez: %1$d z %2$d osób - Przeczytana przez: %1$d z %2$d osób - Przeczytana przez: %1$d z %2$d osób - + Przeczytana przez: %1$d z %2$d osób %d wiadomość %d wiadomości @@ -327,6 +374,9 @@ Brak zadań domowych Wykonane Niewykonane + Dodaj zadanie domowe + Zadanie domowe pomyślnie dodane + Zadanie domowe pomyślnie usunięte Załączniki Nowe zadanie domowe @@ -402,6 +452,8 @@ Masz %1$d nowych zebrań Masz %1$d nowych zebrań + Obecność na zebraniu + Agenda Ogłoszenia szkolne Brak ogłoszeń szkolnych @@ -474,7 +526,7 @@ Imiona matki i ojca Telefon Telefon komórkowy - E-mail + Adres e-mail Adres zamieszkania Adres zameldowania Adres korespondencyjny @@ -496,6 +548,7 @@ Lekcje (Jutro) + (Dzisiaj i jutro) Za chwilę: Wkrótce: Pierwsza: @@ -516,10 +569,10 @@ Następnie: Później: - jeszcze %1$d dodatkowa lekcja - jeszcze %1$d dodatkowe lekcje - jeszcze %1$d dodatkowych lekcji - jeszcze %1$d dodatkowych lekcji + jeszcze %1$d lekcja + jeszcze %1$d lekcje + jeszcze %1$d lekcji + jeszcze %1$d lekcji do %1$s Brak nadchodzących lekcji @@ -528,10 +581,10 @@ Brak prac domowych do wykonania Wystąpił błąd podczas ładowania zadań domowych - Jeszcze %1$d dodatkowe zadanie domowe - Jeszcze %1$d dodatkowe zadania domowe - Jeszcze %1$d dodatkowych zadań domowych - Jeszcze %1$d dodatkowych zadań domowych + Jeszcze %1$d zadanie domowe + Jeszcze %1$d zadania domowe + Jeszcze %1$d zadań domowych + Jeszcze %1$d zadań domowych do %1$s Ostatnie oceny @@ -541,19 +594,19 @@ Brak aktualnych ogłoszeń Wystąpił błąd podczas ładowania ogłoszeń - Jeszcze %1$d dodatkowe ogłoszenie - Jeszcze %1$d dodatkowe ogłoszenia - Jeszcze %1$d dodatkowych ogłoszeń - Jeszcze %1$d dodatkowych ogłoszeń + Jeszcze %1$d ogłoszenie + Jeszcze %1$d ogłoszenia + Jeszcze %1$d ogłoszeń + Jeszcze %1$d ogłoszeń Sprawdziany Brak nadchodzących sprawdzianów Wystąpił błąd podczas ładowania sprawdzianów - Jeszcze %1$d dodatkowy sprawdzian - Jeszcze %1$d dodatkowe sprawdziany - Jeszcze %1$d dodatkowych sprawdzianów - Jeszcze %1$d dodatkowych sprawdzianów + Jeszcze %1$d sprawdzian + Jeszcze %1$d sprawdziany + Jeszcze %1$d sprawdzianów + Jeszcze %1$d sprawdzianów Zebrania Brak nadchodzących zebrań @@ -562,7 +615,7 @@ Jeszcze %1$d dodatkowe zebranie Jeszcze %1$d dodatkowe zebrania Jeszcze %1$d dodatkowych zebrań - Jeszcze %1$d dodatkowych zebrań + Jeszcze %1$d zebrań Wystąpił błąd podczas ładowania danych Brak @@ -590,6 +643,11 @@ Tak Nie Zapisz + Tytuł + Dodaj + Skopiowano + Cofnij + Zmień Brak lekcji Wybierz motyw @@ -597,13 +655,13 @@ Ciemny Motyw systemu - Wygląd i zachowanie aplikacji + Aplikacja Domyślny widok - Obliczanie średniej końcoworocznej + Opcje obliczonej średniej Wymuś obliczanie średniej przez aplikację Pokazuj obecność Motyw - Rozwiń oceny + Rozwijanie ocen Oznaczaj bieżącą lekcję Pokazuj grupę obok przedmiotu Pokazuj listę wykresów w ocenach klasy @@ -612,14 +670,21 @@ Sortowanie przedmiotów Język Powiadomienia + Inne Pokazuj powiadomienia Pokazuj powiadomienia o nadchodzących lekcjach + Ustaw powiadomienie o nadchodzącej lekcji jako trwałe + Wyłącz, gdy powiadomienie nie jest widoczne na zegarku/opasce Otwórz systemowe ustawienia powiadomień Napraw problemy z synchronizacją i powiadomieniami Na twoim urządzeniu mogą występować problemy z synchronizacją danych i powiadomieniami.\n\nBy je naprawić, dodaj Wulkanowego do autostartu i wyłącz optymalizację/oszczędzanie baterii w ustawieniach systemowych telefonu. Przejdź do ustawień Pokazuj powiadomienia debugowania Synchronizacja jest wyłączona + Przechwytywanie powiadomień oficjalnej aplikacji + Przechwytywanie powiadomień + Dzięki tej funkcji możesz uzyskać namiastkę powiadomień push, takich jak w oficjalnej aplikacji. Wszystko, co musisz zrobić, to zezwolić Wulkanowemu na odczytywanie wszystkich powiadomień w ustawieniach systemowych.\n\nJak to działa?\nKiedy otrzymasz powiadomienie w Dzienniczku VULCAN, Wulkanowy zostanie o tym powiadomiony (do tego jest to dodatkowe uprawnienie) i uruchomi synchronizację, aby mógł wysłać własne powiadomienie.\n\nWYŁĄCZNIE DLA ZAAWANSOWANYCH UŻYTKOWNIKÓW + Przejdź do ustawień Synchronizacja Automatyczna aktualizacja Zawieszona na wakacjach @@ -634,16 +699,25 @@ Wartość minusa Odpowiadaj z historią wiadomości Licz średnią arytmetyczną, gdy żadna ocena nie ma wagi + Wsparcie + Obejrzyj pojedynczą reklamę, aby wesprzeć projekt + Zgoda na przetwarzanie danych + Aby obejrzeć reklamę, musisz zaakceptować warunki przetwarzania danych zawarte w naszej Polityce Prywatności + Akceptuję + Polityka prywatności + Ładowanie reklamy Zaawansowane Wygląd i zachowanie Powiadomienia Synchronizacja + Reklamy Oceny Start Widoczność kafelków Frekwencja Plan lekcji Oceny + Obliczona średnia Wiadomości Wygląd i zachowanie Języki, motywy, sortowanie przedmiotów @@ -653,7 +727,8 @@ Automatyczna aktualizacja, interwał synchronizacji Wartości plusa i minusa, obliczanie średniej Zaawansowane - Wersja aplikacji, twórcy, media społecznościowe, licencje + Wersja aplikacji, twórcy, media społecznościowe + Wyświetlanie reklam, wsparcie projektu Nowe oceny Nowe zadania domowe @@ -666,6 +741,8 @@ Powiadomienia push Nadchodzące lekcje Debugowanie + Zmiany planu lekcji + Nowe frekwencje Czarny Czerwony @@ -673,10 +750,6 @@ Zielony Fioletowy Brak koloru - - Skopiowano - Cofnij - Zmień Rozpoczęto pobieranie aktualizacji… Aktualizacja została pobrana. @@ -693,4 +766,5 @@ Wystąpił nieoczekiwany błąd Funkcja wyłączona przez szkołę Funkcja niedostępna. Zaloguj się w trybie innym niż Mobilne API + To pole jest wymagane diff --git a/app/src/main/res/values-ru/preferences_values.xml b/app/src/main/res/values-ru/preferences_values.xml index 7c4d14df..cfdaa957 100644 --- a/app/src/main/res/values-ru/preferences_values.xml +++ b/app/src/main/res/values-ru/preferences_values.xml @@ -40,9 +40,14 @@ Wulkanowy Цвета оценок в дневнике + + До 1 за раз + Всегда развернуто + Неограниченные расширения + - Средняя оценка со 2 семестра - Средняя оценка с двух семестров + Средние оценки только с выбранного семестра + Средние значения для обоих семестров Средняя оценок со всего года diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 09b8a146..c5dc73e3 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -17,6 +17,7 @@ Лицензии Сообщения Новое сообщение + Новая домашняя работа Предупреждения и свершения Домашние задания Менеджер аккаунтов @@ -24,11 +25,12 @@ Данные аккаунта Информация о студенте Панель + Центр уведомлений %1$d семестр, %2$d/%3$d Авторизируйтесь при помощи аккаунта ученика или родителя - Введите символ со страницы регистрации + Введите символ со страницы регистрации: <b>%1$s</b> Имя пользователя Электронная почта Логин, PESEL или электронная почта @@ -42,7 +44,8 @@ Symbol Войти Слишком короткий пароль - Данные для входа неверны. Убедитесь, что в поле ниже выбран правильный вариант регистра UONET+ + Данные для входа указаны неверно + %1$s. Убедитесь, что ниже выбран правильный UONET+ вариант регистра Неправильный PIN Неверный token Token просрочен @@ -51,7 +54,6 @@ Использовать назначенный логин или email в @%1$s Неправильный символ Студент не найден. Подтвердите символ и выбранный вариант регистра UONET+ - Это обязательное поле Данный ученик уже авторизован Этот символ можно найти на странице регистрации в  Ученик →  Телефонный доступ →   Зарегистрируйте мобильное устройство.\n\nУбедитесь, что вы установили соответствующий вариант регистра в поле Разновидностью бревна UONET+ на предыдущем экране. Вулкановый на данный момент не обнаруживает дошкольников Выберите учеников для авторизации в приложении @@ -90,6 +92,10 @@ Итоговая оценка Ожидаемая оценка Рассчитанная средняя оценка + Как рассчитывается средняя работа? + Расчетное среднее - это среднее арифметическое, рассчитанное на основе средних значений испытуемых. Это позволяет узнать приблизительное итоговое среднее значение. Он рассчитывается способом, выбранным пользователем в настройках приложения. Рекомендуется выбрать подходящий вариант. Это потому, что расчет средних показателей школы отличается. Кроме того, если ваша школа сообщает среднее значение по предметам на странице Vulcan, приложение загружает их и не вычисляет эти средние значения. Это можно изменить, принудительно вычисляя среднее значение в настройках приложения.\n\nСреднее значение только за выбранный семестр :\n1. Вычисление средневзвешенного значения по каждому предмету за семестр\n2.Добавление вычисленных средних\n3. Вычисление среднего арифметического суммарных средних\n\nСреднее из средних значений за оба семестра:\n1.Расчет средневзвешенного значения для каждого предмета в семестрах 1 и 2\n2. Вычисление среднего арифметического рассчитанных средних значений для семестров 1 и 2 по каждому предмету.\n3. Добавление вычисленных средних\n4. Расчет среднего арифметического суммированных средних\n\nСреднее значение оценок за весь год: \n1. Расчет средневзвешенного значения за год по каждому предмету. Итоговое среднее значение за 1 семестр не имеет значения.\n3. Добавление вычисленных средних\n4. Расчет среднего арифметического + Как работает окончательный средний показатель? + Среднее арифметическое - это среднее арифметическое, рассчитанное по всем имеющимся на данный момент итоговым классам данного семестра.\n\nСхема расчета состоит из следующих шагов:\n1. Суммирование итоговых классов преподавателей\n2. Деление по количеству уже оцененных предметов Итоговая средняя оценка от %1$d из %2$d субъектов Итоги @@ -159,6 +165,34 @@ Сейчас: %s Следующий: %s Позже: %s + %1$s урок %2$d - %3$s + Изменить комнату с %1$s на %2$s + Изменить учителя с %1$s на %2$s + Изменить тему с %1$s на %2$s + + Изменение расписания + Изменение расписания + Изменение расписания + Изменение расписания + + + %1$s - %2$d изменений в расписании + %1$s - %2$d изменений в расписании + %1$s - %2$d изменений в расписании + %1$s - %2$d изменений в расписании + + + %1$d - изменений в расписании + %1$d изменение в расписании + %1$d изменение в расписании + %1$d изменений в расписании + + + %d изменение + %d изменение + %d изменение + %d изменений + Проведённые уроки Просмотреть проведённые уроки @@ -188,6 +222,24 @@ Запрос на освобождение оправдания успешно отправлен! Выберите хотя-бы одно отсутствие Изменить статус + + Новое посещение + Новое посещение + Новое посещение + Новое посещение + + + %1$d новое посещения + %1$d новое посещение + %1$d новое посещение + %1$d новое посещения + + + %d посещаемость + %d посещаемость + %d посещаемость + %d посещаемость + Общая @@ -238,12 +290,7 @@ Только непрочитанные Только с вложениями Чтение: %s - - Прочитано: %1$d из %2$d человек - Прочитано: %1$d из %2$d человек - Прочитано: %1$d из %2$d человек - Прочитано: %1$d из %2$d человек - + Прочитано: %1$d из %2$d человек %d сообщение %d сообщения @@ -327,6 +374,9 @@ Нет домашних заданий Отметить как выполненное Отметить как невыполненное + Добавить домашнюю работу + Домашняя работа успешно добавлена + Домашняя работа успешно удалена Вложения Новая домашняя работа @@ -402,6 +452,8 @@ У вас %1$d новая конференция У вас %1$d новых конференций + Присутствует на конференции + Повестка дня Объявления школ Нет объявлений о школе @@ -496,6 +548,7 @@ Уроки (Завтра) + (Сегодня и завтра) Сейчас: Скоро: Первый: @@ -590,6 +643,11 @@ Да Нет Сохранить + Тема + Добавить + Скопировано + Отменить + Изменить Нет уроков Выбрать тему @@ -597,13 +655,13 @@ Тёмная Тема системы - Внешний вид приложения & поведение + Приложение Окно по умолчанию - Способ определения средней годовой оценки + Рассчитанные средние параметры Принудительно высчитать среднюю оценку через приложение Показать присутствие Тема - Разворачивать оценки + Расширяется оценка Отметить текущий урок Показать группы рядом с темами Показывать диаграммы в оценках класса @@ -612,14 +670,21 @@ Сортировка уроков Язык Уведомления + Прочее Показывать уведомления Показывать уведомления о будущих уроках + Сделать уведомления о предстоящем уроке постоянным + Выключить, когда уведомление не отображается в чата/полосе Открыть настройки уведомлений системы Исправить проблемы с синхронизацией и уведомлениями На вашем устройстве могут быть проблемы с синхронизацией данных и уведомлениями.\n\nЧтобы их исправить, вам необходимо добавить Wulkanowy в авто-старт и выключить оптимизацию/экономию батареи в настройках устройства. Перейти в настройски Показывать дебаг-уведомления Синхронизация отключена + Записывать официальные уведомления + Показывать push-уведомления + С помощью этой функции вы можете получить замену push-уведомлений, как в официальном приложении. Все, что вам нужно сделать, это разрешить Wulkanowy получать все уведомления в настройках системы.\n\nКак это работает?\nКогда вы получаете уведомление в Dziennik VULCAN, Wulkanowy будет уведомлен (это требует дополнительных прав) и запустит синхронизацию, чтобы отправить свое уведомление.\n\nТОЛЬКО ДЛЯ ПОЛЬЗОВАТЕЛЯ + Перейти к настройкам Синхронизация Автоматическая синхронизация Приостановить синхронизации во время каникул @@ -634,16 +699,25 @@ Стоимость минуса Отвечать с историей сообщений Показывать среднее арифметическое при отсутствии весов + Поддержка + Смотреть одиночную рекламу для поддержки проекта + Согласие на обработку данных + Для просмотра рекламы вы должны согласиться с условиями обработки данных нашей Политики конфиденциальности + Согласен + Политика конфиденциальности + Объявление загружается Расширенные Внешний вид & Поведение Уведомления Синхронизация + Реклама Оценки Панель Видимость плиток Посещаемость Расписание Оценки + Расчетное среднее Сообщения Внешний вид & Поведение Языки, темы, темы сортировки темы @@ -653,7 +727,8 @@ Автоматическое обновление, интервал синхронизации Значения плюс и минус, средний расчет Расширенные - Версия приложения, участники, социальные порталы, лицензии + Версия приложения, участники, социальные порталы + Отображение объявлений, поддержка проекта Новые оценки Новая домашняя работа @@ -666,6 +741,8 @@ Показывать push-уведомления Будущие уроки Дебаг + Изменение расписания + Новое посещение Чёрный Красный @@ -673,10 +750,6 @@ Зелёный Фиолетовый Нет цвета - - Скопировано - Отменить - Изменить Загрузка обновлений началась… Только что было скачано обновление. @@ -693,4 +766,5 @@ Произошла неожиданная ошибка Функция была выключена школой Функция не доступна в этом режиме + Это поле является обязательным diff --git a/app/src/main/res/values-sk/preferences_values.xml b/app/src/main/res/values-sk/preferences_values.xml index 108af555..e64f5606 100644 --- a/app/src/main/res/values-sk/preferences_values.xml +++ b/app/src/main/res/values-sk/preferences_values.xml @@ -40,15 +40,20 @@ Wulkanowy Farby známok v denníku + + Až 1 naraz + Vždy rozbalené + Neobmedzené rozbalené + - Priemer známok až od druhého semestra - Priemer známok z oboch semestrov + Priemer známok iba z vybraného semestra + Priemer z priemerov z oboch semestrov Priemer známok z celého roka Šťastné číslo Neprečítané správy - Dochádzka + Frekvencia Lekcie Známky Domáce úlohy diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index ad078e33..fc459eaf 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -4,7 +4,7 @@ Prihlásenie Wulkanowy Známky - Dochádzka + Frekvencia Skúšky Plán lekcie Nastavenia @@ -12,11 +12,12 @@ O aplikácii Prehliadač protokolov Ladenie - Ladenie oznámení - Prispievatelia + Ladenie upozornení + Tvorcovia Licencie Správy Nová správa + Nový domáci úloh Poznámky a úspechy Domáce úlohy Manažér účtov @@ -24,11 +25,12 @@ Podrobnosti účtu Informácie o žiakovi Domov + Centrum upozornení Semester %1$d, %2$d/%3$d Prihláste sa pomocou študentského alebo rodičovského konta - Zadajte symbol zo stránky denníka + Zadajte symbol zo stránky denníka: <b>%1$s</b> Užívateľské meno Email Prihlásenie, číslo PESEL alebo e-mail @@ -42,7 +44,8 @@ Symbol Prihlásiť Toto heslo je príliš krátke - Prihlasovacie údaje sú nesprávne. Uistite sa, že je v poli nižšie vybraná správna variácie denníka UONET+ + Prihlasovacie údaje sú nesprávne + %1$s. Skontrolujte, či je nižšie vybratá správna variácie denníka UONET+ Neplatný PIN Neplatný token Platnosť tokenu vypršala @@ -51,15 +54,14 @@ Použite priradené prihlasovacie alebo e-mail v @%1$s Neplatný symbol Žiak nebol nájdený. Skontrolujte správnosť symbolu a vybrané varianty denníka UONET+ - Toto pole je povinné Vybraný žiak už je prihlásený Symbol nájdete na stránke denníka v  Uczeń→ Dostęp Mobilny → Zarejestruj urządzenie mobilne.\n\nUistite sa, že ste na predchádzajúcu obrazovke nastaviť správny variant denníka do poľa Variácie denníka UONET+. Wulkanowy v túto chvíľu nezistí predškolské żaków Vyberte žiakov, ktorí sa majú do aplikácie prihlásiť Iné možnosti - V tomto režime nefungujú nasledovné: šťastné číslo, štatistiky triedy, zhrnutie dochádzky, ospravedlnenie neprítomnosti, dokončené lekcie, informácie o škole a prezeranie zoznamu registrovaných zariadení + V tomto režime nefungujú nasledovné: šťastné číslo, štatistiky triedy, zhrnutie frekvencií, ospravedlnenie neprítomnosti, dokončené lekcie, informácie o škole a prezeranie zoznamu registrovaných zariadení Tento režim zobrazuje rovnaké dáta, ktoré sa zobrazujú na webových stránkach denníka Kombinácia najlepších vlastností ostatných dvoch režimov. Funguje rýchlejšie ako scraper a poskytuje funkcie, ktoré nie sú k dispozícii v režime Mobilne API. Je to v experimentálnej fáze - Zásady ochrany osobných údajov + Ochrana osobných údajov Problémy s prihlásením? Napíšte nám! Email Discord @@ -90,6 +92,10 @@ Konečná známka Predpokladaná 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ý.\n3. Sčítanie vypočítaných priemerov\n4. Výpočet aritmetického priemeru součtených priemerov + Ako funguje konečný priemer? + Konečný priemer je aritmetický priemer vypočítaný zo všetkých aktuálne dostupných konečných známok v danom semestri.\n\nSchéma výpočtu sa skladá z nasledujúcich krokov:\n1. Sčítanie konečných známok zadaných učiteľmi\n2. Delené počtom predmetov, pre ktoré už boli vydané známky Konečný priemer z %1$d z %2$d predmetov Zhrnutie @@ -159,6 +165,34 @@ Teraz: %s Za chvíľu: %s Neskôr: %s + %1$s lekcia %2$d - %3$s + Zmena učebne z %1$s na %2$s + Zmena učiteľa z %1$s na %2$s + Zmena predmetu z %1$s na %2$s + + Zmena plánu lekcií + Zmeny plánu lekcií + Zmeny plánu lekcií + Zmeny plánu lekcií + + + %1$s - %2$d zmena plánu lekcií + %1$s - %2$d zmeny plánu lekcií + %1$s - %2$d zmien plánu lekcií + %1$s - %2$d zmien plánu lekcií + + + %1$d zmena plánu lekcií + %1$d zmeny plánu lekcií + %1$d zmien plánu lekcií + %1$d zmien plánu lekcií + + + %d zmena + %d zmeny + %d zmien + %d zmien + Dokončené lekcie Zobraziť dokončené lekcie @@ -167,11 +201,11 @@ Neprítomnosť Zdroje - Ďalší lekcie + Ďalšie lekcie Zobraziť ďalšie lekcie Žiadne informácie o ďalších lekciách - Zhrnutie dochádzky + Zhrnutie frekvencií Neprítomnosť zo školských dôvodov Ospravedlnená neprítomnosť Neospravedlnená neprítomnosť @@ -188,6 +222,24 @@ Žiadosť o ospravedlnenie neprítomnosti bola úspešne odoslaná! Musíte vybrať aspoň jednu neprítomnosť! Ospravedlniť + + Nová frekvencia + Nové frekvencie + Nové frekvencie + Nové frekvencie + + + %1$d nová frekvencia + %1$d nové frekvencie + %1$d nových frekvencií + %1$d nových frekvencií + + + %d frekvencia + %d frekvencie + %d frekvencií + %d frekvencií + Spoločne @@ -238,12 +290,7 @@ Iba neprečítané Iba s prílohami Prečítaná: %s - - Prečítaná cez: %1$d z %2$d osôb - Prečítaná cez: %1$d z %2$d osôb - Prečítaná cez: %1$d z %2$d osôb - Prečítaná cez: %1$d z %2$d osôb - + Prečítaná cez: %1$d z %2$d osôb %d správa %d správy @@ -327,6 +374,9 @@ Žiadne informácie o domácich úlohách Označiť ako hotové Nevyrobené + Pridať domácu úlohu + Domáca úloha bola úspešně pridaná + Domáca úloha bola úspešně odstránená Prílohy Nový domáci úloh @@ -402,6 +452,8 @@ Máte %1$d nových stretnutí Máte %1$d nových stretnutí + Prítomnosť na stretnutí + Agenda Školské oznámenia Žiadne školské oznámenia @@ -439,7 +491,7 @@ Osobné údaje Verzia aplikácie - Prispievatelia + Tvorcovia Zoznam vývojárov Wulkanového Nahlásiť chybu Odoslať správu o chybe e-mailom @@ -447,11 +499,11 @@ Prečítajte si často kladené otázky Server Discord Pripojte sa ku komunite Wulkanového - Facebooková fanpage + Stránka na Facebooku Twitter stránka Sledujte nás na Twitteri - Rovnako ako naše facebooková fanpage - Zásady ochrany osobných údajov + Dajte like našej stránke na Facebooku + Ochrana osobných údajov Pravidlá pre zhromažďovanie osobných údajov Systémové nastavenia Otvoriť systémové nastavenia @@ -496,6 +548,7 @@ Lekcie (Zajtra) + (Dnes a zajtra) Za chvíľu: Čoskoro: Prvá: @@ -516,10 +569,10 @@ Ďalej: Neskôr: - ešte %1$d ďalší lekcia - ešte %1$d ďalšie lekcie - ešte %1$d ďalších lekcií - ešte %1$d ďalších lekcií + ešte %1$d lekcia + ešte %1$d lekcie + ešte %1$d lekcií + ešte %1$d lekcií do %1$s Žiadne nadchádzajúce lekcie @@ -528,10 +581,10 @@ Žiadne domáce úlohy do vykonaná Pri načítaní domácich úloh došlo k chybe - Ešte %1$d ďalšia domáca úloha - Ešte %1$d ďalšie domáce úlohy - Ešte %1$d ďalších domácich úloh - Ešte %1$d ďalších domácich úloh + Ešte %1$d domáca úloha + Ešte %1$d domáce úlohy + Ešte %1$d domácich úloh + Ešte %1$d domácich úloh do %1$s Posledné známky @@ -541,28 +594,28 @@ Žiadne aktuálne oznámenia Pri načítaní oznámení došlo k chybe - Ešte %1$d ďalšie oznámenie - Ešte %1$d ďalšie oznámenia - Ešte %1$d ďalších oznámení - Ešte %1$d ďalších oznámení + Ešte %1$d oznámenie + Ešte %1$d oznámenia + Ešte %1$d oznámení + Ešte %1$d oznámení Skúšky Žiadne nadchádzajúce skúšky Pri načítaní skúšok došlo k chybe - Ešte %1$d ďalšia skúška - Ešte %1$d ďalšie skúšky - Ešte %1$d ďalších skúšok - Ešte %1$d ďalších skúšok + Ešte %1$d skúška + Ešte %1$d skúšky + Ešte %1$d skúšok + Ešte %1$d skúšok Stretnutie Žiadna nadchádzajúce stretnutie Pri načítaní stretnutí došlo k chybe - Ešte %1$d ďalšie stretnutie - Ešte %1$d ďalšie stretnutia - Ešte %1$d ďalších stretnutí - Ešte %1$d ďalších stretnutí + Ešte %1$d stretnutie + Ešte %1$d stretnutia + Ešte %1$d stretnutí + Ešte %1$d stretnutí Pri načítaní dát došlo k chybe Žiadne @@ -590,6 +643,11 @@ Áno Nie Uložiť + Titul + Pridať + Skopírované + Vrátiť + Zmeniť Žiadne lekcie Vybrať motív @@ -597,13 +655,13 @@ Tmavý Motív systému - Vzhľad a správanie aplikácií + Aplikácia Predvolené zobrazenie - Výpočet koncoročního priemeru + Možnosti vypočítaného priemeru Vynútiť priemerný výpočet podľa aplikácie Zobraziť prítomnosť Motív - Rozbaliť známky + Rozvijanie známok Označiť aktuálne lekciu Zobraziť skupiny vedľa predmetov Zobraziť zoznam grafov v známkach triedy @@ -612,14 +670,21 @@ Triedenie predmetov Jazyk Upozornenia + Iné Zobraziť upozornenia Zobraziť upozornenia o nadchádzajúcej lekciu + Nastaviť upozornenia o nadchádzajúcej lekcii ako trvalé + Vypnúť, keď upozornenia nie je vo vašom hodinkách/náramku viditeľné Otvoriť systémové nastavenia upozornení Opravte problémy so synchronizáciou a upozornením Vaše zariadenie môže mať problémy so synchronizáciou dát as upozorneniami.\n\nAk ich chcete opraviť, pridajte Wulkanového do funkcie Autostart a vypnite optimalizáciu/úsporu batérie v nastavení systému telefóne. Prejsť do nastavení Zobraziť upozornenia o ladení Synchronizácia je vypnutá + Zachytiť upozornenia oficiálnej aplikácie + Zachytiť upozornenia + S touto funkciou môžete získať náhradu push upozornení ako v oficiálnej aplikácii. Všetko, čo musíte urobiť, je povoliť Wulkanowému čítať všetky vaše upozornenia v nastaveniach systému.\n\nAko to funguje?\nKeď dostanete oznámenie v Deníčku VULCAN, Wulkanowy bude o tom informovaný (k tomu je to dodatočné povolenie) a spustí synchronizáciu, aby mohol zaslať vlastné upozornenie.\n\nLEN PRE POKROČILÝCH POUŽĺVATEĹOV + Prejsť do nastavení Synchronizácia Automatická aktualizácia Pozastavený počas dovolenky @@ -634,16 +699,25 @@ Hodnota mínusu Odpovedať s históriou správ Vypočítať aritmetický priemer, ak žiadna známka nemá váhu + Podpora + Pozrite sa na jednu reklamu pre podporu projektu + Súhlas so spracovaním dát + Ak chcete sledovať reklamu, musíte súhlasiť s podmienkami spracovania údajov v našich Zásadách Ochrany Osobných Údajov + Súhlasím + Ochrana osobných údajov + Reklama sa načítava Pokročilé Vzhľad a správanie Upozornenia Synchronizácia + Reklamy Známky Domov Viditeľnosť dlaždíc - Dochádzka + Frekvencia Plán lekcie Známky + Vypočítaný priemer Správy Vzhľad a správanie Jazyky, motívy, triedenie predmetov @@ -653,7 +727,8 @@ Automatická aktualizácia, interval aktualizácií Hodnota plusu a mínusu, výpočet priemeru Pokročilé - Verzia aplikácie, prispievatelia, sociálne portály, licencie + Verzia aplikácie, tvorcovia, sociálne portály + Zobrazovanie reklám, podpora projektu Nové známky Nové domáce úlohy @@ -666,6 +741,8 @@ Push upozornenia Nadchádzajúce lekcie Ladenie + Zmeny plánu lekcií + Nové frekvencie Čierna Červená @@ -673,10 +750,6 @@ Zelená Fialová Žiadna farba - - Skopírované - Vrátiť - Zmeniť Sťahovanie aktualizácií začalo… Aktualizácia bola stiahnutá. @@ -693,4 +766,5 @@ 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 + Toto pole je povinné diff --git a/app/src/main/res/values-uk/preferences_values.xml b/app/src/main/res/values-uk/preferences_values.xml index f6f5b984..a8c09bcf 100644 --- a/app/src/main/res/values-uk/preferences_values.xml +++ b/app/src/main/res/values-uk/preferences_values.xml @@ -40,9 +40,14 @@ Wulkanowy Кольори оцінок в щоденнику + + Раз до 1 + Завжди розгорнутий + Необмежена кількість розширень + - Середня оцінка з 2 семестру - Середнє оцінювання за обидва семестри + Середні оцінки тільки від обраного семестру + Середнє значення для обох семестів Середнє оцінювання за весь рік diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 41f1c300..af774cdd 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -17,6 +17,7 @@ Ліцензії Повідомлення Нове повідомлення + Нова домашня робота Нотатки та досягнення Домашні завдання Менеджер аккаунтів @@ -24,11 +25,12 @@ Деталі облікового запису Інформація про учня Дошка + Журнал сповіщень %1$d семестр, %2$d/%3$d Авторизуйтеся за допомогою аккаунта учня або батьків - Введіть символ зі сторінки реєстру + Введіть символ зі сторінки реєстру: <b>%1$s</b> Ім\'я користувача Електронна пошта Логін, PESEL або електронна пошта @@ -42,7 +44,8 @@ Symbol Увійти Занадто короткий пароль - Дані для входу неправильні. Переконайтеся, що у полі нижче вказано правильний варіант реєстрації UONET+ + Вказані невірні дані + %1$s. Переконайтеся, що обрано правильну варіацію запису UONET+ Неправильний PIN Неправильний token Минув термін дії токену @@ -51,7 +54,6 @@ Використовуйте призначений логін або електронну адресу в @%1$s Неправильний симбвол Студента не знайдено Перевірте символ та обраний варіант реєстру UONET+ - Обов\'язкове поле Даного учня вже авторизовано Символ можна знайти на сторінці реєстру в   Учень →   Мобільний доступ →   Додайте мобільне приладдя .\n\nПереконайтесь, що ви встановили відповідний варіант реєстру в полі UONET + варіант реєстрації на попередньому екрані. На даний момент Wulkanowy не виявляє учнів дошкільних закладів Виберіть учнів для авторизації в додатку @@ -90,6 +92,10 @@ Підсумкова оцінка Очікувана оцінка Розрахована середня оцінка + Як розраховується середньо? + Розрахункове середнє - це середнє арифметичне, обчислене з середніх показників для випробуваних. Це дозволяє дізнатися приблизну кінцеву середню величину. Він розраховується способом, обраним користувачем у налаштуваннях програми. Рекомендується вибрати відповідний варіант. Це пояснюється тим, що розрахунок середніх показників за школою відрізняється. Крім того, якщо у вашій школі повідомляється середнє значення предметів на сторінці Вулкан, програма завантажує їх і не обчислює ці середні значення. Це можна змінити шляхом примусового розрахунку середнього значення в налаштуваннях програми.\n\nСередні оцінки лише за вибраний семестр :\n1. Розрахунок середньозваженого для кожного предмета в даному семестрі\n2.Додавання розрахункових середніх\n3. Розрахунок середнього арифметичного підсумованих середніх значень\n\nСереднє значення середніх показників за обидва семестри :\n1.Обчислення середньозваженого значення для кожного предмета у 1 та 2 семестрах\n2. Обчислення середнього арифметичного розрахункових середніх показників за 1 та 2 семестри для кожного предмета.\n3. Додавання розрахункових середніх\n4. Розрахунок середнього арифметичного підсумованих середніх значень\n\nСереднє значення оцінок за весь рік: \n1. Розрахунок середньозваженого показника за рік для кожного предмета. Остаточний середній показник у 1 -му семестрі не має значення.\n3. Додавання розрахункових середніх \n4. Обчислення середнього арифметичного середніх суммованих середніх + Як працює кінцевий середній показник? + Підсумкове середнє значення - це середнє арифметичне, обчислене з усіх наявних наразі підсумкових оцінок у даному семестрі. \ N \ nСхема обчислення складається з таких кроків: \ n1. Підбиття підсумкових оцінок викладачів \ n2. Поділіть на кількість предметів, які вже оцінені Підсумкова середня оцінка з %1$d із %2$d тем Підсумок @@ -159,6 +165,34 @@ Зараз: %s Наступний: %s Пізніше: %s + %1$s урок %2$d - %3$s + Зміна місця з %1$s на %2$s + Змінити вчителя з %1$s на %2$s + Зміна теми з %1$s на %2$s + + Зміна у розкладі + Зміна у розкладі + Зміна у розкладі + Зміни у розкладі рейсу + + + %1$s - %2$d зміна в розкладі + %1$s - %2$d зміна в розкладі + %1$s - %2$d зміна в розкладі + %1$s - %2$d змін у розкладі + + + %1$d зміна в розкладі + %1$d зміна в розкладі + %1$d зміна в розкладі + %1$d змін у розкладі + + + %d зміна + %d зміна + %d зміна + %d змін + Уроки, що відбулися Показати уроки, що відбулися @@ -188,6 +222,24 @@ Запит на виправдання відсутності успішно надіслано! Оберіть хоча б одну відсутність Змінити статус + + Нова відвідуваність + Нова відвідуваність + Нова відвідуваність + Нова відвідуваність + + + %1$d новий відвідувач + %1$d новий відвідувач + %1$d новий відвідувач + %1$d відвідування + + + %d відвідування + %d відвідування + %d відвідування + %d відвідування + Загальна @@ -238,12 +290,7 @@ Лише непрочитані Тільки з вкладеннями Читання: %s - - Прочитані: %1$d з %2$d осіб - Прочитані: %1$d з %2$d осіб - Прочитані: %1$d з %2$d осіб - Прочитані: %1$d з %2$d осіб - + Прочитанно:%1$d через %2$d людей %d повідомлення %d повідомлення @@ -327,6 +374,9 @@ Брак домашніх завдань Позначити як зроблене Позначити як не зроблене + Додати домашню роботу + Домашню роботу додано успішно + Домашню роботу видалено успішно Додатки Нова домашня робота @@ -402,6 +452,8 @@ У вас є %1$d нова конференція У вас є %1$d нових конференцій + Присутність на конференції + Порядок денний Оголошення школи Жодних навчальних оголошень @@ -496,6 +548,7 @@ Уроки (Завтра) + (сьогодні та завтра) Через мить: Незабаром: Перше: @@ -590,6 +643,11 @@ Так Ні Зберегти + Титул + Додати + Скопійовано + Відмінити + Змінити Брак уроків Увібрати тему @@ -597,13 +655,13 @@ Темна Тема системи - Поява додатка & amp; поведінки + Додатки Вікно за замовчуванням - Спосіб облічування оцінки на кінець року + Розрахункові середні параметри Примусово розрахувати середню оцінку через додаток Показати присутність Тема - Більше оцінок + Розширення оцінок Позначити поточний урок Показувати групи поруч з темами Показувати діаграми в оцінках класу @@ -612,14 +670,21 @@ Сортування предметів Мова Повідомлення + Інше Показувати повідомлення Показувати повідомлення о наступних уроках + Зробити сповіщення майбутнього уроку нестійкими + Вимкнути коли сповіщення не показуються у відстежувачі/темпі Відкрити налаштування сповіщень системи Виправити помилки з синхронізацією і повідомленнями На вашому пристрої можуть бути помилки з синхронізацією і повідомленнями\n\nЩоб виправити іх, вам необхідно додати Wulkanowy в авто-старт и вимкнути оптимізацію/экономію батареї в налаштуваннях пристрою. Перейти до налаштувань Показувати дебаг-повідомлення Синхронізація вимкнена + Захоплювати офіційні сповіщення програм + Показувати push-повідомлення + За допомогою цієї функції ви можете отримати заміну push -повідомлень, як у офіційному додатку. Все, що вам потрібно зробити, це дозволити Wulkanowy отримувати всі сповіщення у налаштуваннях вашої системи. \ N \ nЯк це працює? \ NКоли ви отримаєте сповіщення у Dziennik VULCAN, Wulkanowy отримає сповіщення (для цього призначені ці додаткові дозволи) і запустить синхронізація, яка може надсилати власне сповіщення. \ n \ n ТІЛЬКИ ДЛЯ РОЗШИРЕНИХ КОРИСТУВАЧІВ + Перейти до налаштувань Синхронізація Автоматична синхронізація Призупинено на час канікул @@ -634,16 +699,25 @@ Вага мінуса Відповісти з історією повідомлень Показувати в середньому арифметику, якщо немає ваги + Підтримка + Відстежуйте єдину рекламу для підтримки проекту + Згода в обробці даних + Щоб переглянути рекламу, ви повинні погодитися з умовами обробки даних нашої Політики конфіденційності + Погоджуюсь + Політика конфіденційності + Реклама завантажується Додатково Вигляд & Поведінка Повідомлення Синхронізація + Реклама Оцінки Дошка Видимість плиток Відвідуваність Розклад Класи + Обчислена середня Повідомлення Вигляд & Поведінка Мови, теми, тема сортування @@ -653,7 +727,8 @@ Автоматичне оновлення, інтервал синхронізації Плюс і мінус значення, середні обчислення Додатково - Версія програми, учасники, соціальні портали, ліцензії + Версія програми, учасники, соціальні портали + Відображається реклама, підтримка проектів Нові оцінки Нова домашня робота @@ -666,6 +741,8 @@ Показувати push-повідомлення Наступні уроки Дебаг + Зміна у розкладі + Нова відвідуваність Чорний Червоний @@ -673,10 +750,6 @@ Зелений Фіолетовий Брак кольору - - Скопійовано - Відмінити - Змінити Завантаження оновлень розпочато… Щойно завантажено оновлення. @@ -693,4 +766,5 @@ Відбулася несподівана помилка Функція вимкнена школою Функція не доступна в цьому режимі + Це поле обов\'язкове для заповнення diff --git a/app/src/main/res/values-v23/styles.xml b/app/src/main/res/values-v23/styles.xml index 574e8488..840f5357 100644 --- a/app/src/main/res/values-v23/styles.xml +++ b/app/src/main/res/values-v23/styles.xml @@ -1,14 +1,8 @@ + - - \ No newline at end of file diff --git a/app/src/main/res/values-v26/styles.xml b/app/src/main/res/values-v26/styles.xml index 55413c05..3fb0a5dd 100644 --- a/app/src/main/res/values-v26/styles.xml +++ b/app/src/main/res/values-v26/styles.xml @@ -5,11 +5,4 @@ false @android:color/darker_gray - - diff --git a/app/src/main/res/values-v28/styles.xml b/app/src/main/res/values-v28/styles.xml index ee77091d..a936566f 100644 --- a/app/src/main/res/values-v28/styles.xml +++ b/app/src/main/res/values-v28/styles.xml @@ -6,12 +6,4 @@ true @android:color/white - - \ No newline at end of file diff --git a/app/src/main/res/values-v29/styles.xml b/app/src/main/res/values-v29/styles.xml index ee77091d..a936566f 100644 --- a/app/src/main/res/values-v29/styles.xml +++ b/app/src/main/res/values-v29/styles.xml @@ -6,12 +6,4 @@ true @android:color/white - - \ No newline at end of file diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 49ef39ab..d4ed6e97 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -3,4 +3,5 @@ + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index a68e2710..f3112b10 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -12,6 +12,9 @@ #1C1C1C #0D0D0D + #FFD980 + #ffd54f + #ffd54f #ff8f00 diff --git a/app/src/main/res/values/preferences_defaults.xml b/app/src/main/res/values/preferences_defaults.xml index 1ba9359c..7fb3d5c0 100644 --- a/app/src/main/res/values/preferences_defaults.xml +++ b/app/src/main/res/values/preferences_defaults.xml @@ -4,7 +4,7 @@ true only_one_semester false - false + one false light vulcan @@ -14,6 +14,7 @@ false true false + true false 0.33 0.33 @@ -26,6 +27,7 @@ false false 0 + false LUCKY_NUMBER MESSAGES diff --git a/app/src/main/res/values/preferences_keys.xml b/app/src/main/res/values/preferences_keys.xml index 09dac700..fef062dd 100644 --- a/app/src/main/res/values/preferences_keys.xml +++ b/app/src/main/res/values/preferences_keys.xml @@ -5,7 +5,8 @@ app_theme dashboard_tiles grade_color_scheme - expand_grade + expand_grade + expand_grade_mode grade_average_mode grade_average_always_calc grade_statistics_list @@ -18,6 +19,7 @@ notifications_system_settings notifications_enable notifications_upcoming_lessons_enable + notifications_upcoming_lessons_persistent notification_debug grade_modifier_plus grade_modifier_minus @@ -32,4 +34,6 @@ message_send_is_draft message_send_recipients last_sync_date + notifications_piggyback + single_ad_support diff --git a/app/src/main/res/values/preferences_values.xml b/app/src/main/res/values/preferences_values.xml index 9c1a0421..1d777bdb 100644 --- a/app/src/main/res/values/preferences_values.xml +++ b/app/src/main/res/values/preferences_values.xml @@ -99,9 +99,20 @@ grade_color + + Up to 1 at once + Always expanded + Unlimited expansions + + + one + always + any + + - Average of grades only from the 2nd semester - Average of grades from both semesters + Average of grades only from selected semester + Average of averages from both semesters Average of grades from the whole year diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cf8a0760..6b56e077 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -17,6 +17,7 @@ Licenses Messages New message + New homework Notes and achievements Homework Accounts manager @@ -24,6 +25,7 @@ Account details Student info Dashboard + Notifications center @@ -32,7 +34,7 @@ Sign in with the student or parent account - Enter the symbol from the register page + Enter the symbol from the register page for account: <b>%1$s</b> Username Email Login, PESEL or e-mail @@ -46,7 +48,8 @@ Symbol Sign in Password too short - Login details are incorrect. Make sure the correct UONET+ register variation is selected in the field below + Login details are incorrect + %1$s. Make sure the correct UONET+ register variation is selected below Invalid PIN Invalid token Token expired @@ -55,7 +58,6 @@ Use the assigned login or email in @%1$s Invalid symbol Student not found. Validate the symbol and the chosen variation of the UONET+ register - This field is required Selected student is already logged in The symbol can be found on the register page in Uczeń → Dostęp Mobilny → Zarejestruj urządzenie mobilne.\n\nMake sure that you have set the appropriate register variant in the UONET+ register variant field on the previous screen. Wulkanowy does not detect pre-school students at the moment Select students to log in to the application @@ -69,7 +71,7 @@ Discord Send email Zgłoszenie: Problemy z logowaniem - Informacje o aplikacji:\n\nUrządzenie: %1$s\nWersja SDK: %2$s\nWersja aplikacji: %3$s\nDodatkowe informacje: %4$s\nOstatni błąd: %5$s\n\nOpis problemu: + Informacje o aplikacji:\n\nUrządzenie: %1$s\nWersja SDK: %2$s\nWersja aplikacji: %3$s\nDodatkowe informacje: %4$s\nOstatni błąd: %5$s\n\nPełna nazwa szkoły i klasa ucznia: Make sure you select the correct UONET+ register variation! I forgot my password Recover your account @@ -100,6 +102,10 @@ Final grade Predicted 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.\n3. Adding calculated averages\n4. Calculating the arithmetic average of summed averages + How does the Final Average work? + The Final Average is the arithmetic average calculated from all currently available final grades in the given semester.\n\nThe calculation scheme consists of the following steps:\n1. Summing up the final grades given by teachers\n2. Divide by the number of subjects that have already been graded Final average from %1$d of %2$d subjects Summary @@ -157,6 +163,26 @@ Now: %s Next: %s Later: %s + %1$s lesson %2$d - %3$s + Change of room from %1$s to %2$s + Change of teacher from %1$s to %2$s + Change of subject from %1$s to %2$s + + Timetable change + Timetable changes + + + %1$s - %2$d change in timetable + %1$s - %2$d changes in timetable + + + %1$d change in timetable + %1$d changes in timetable + + + %d change + %d changes + @@ -194,6 +220,18 @@ Excuse z powodu Dzień dobry,\nProszę o usprawiedliwienie mojego dziecka w dniu %s z lekcji %s%s%s.\n\nPozdrawiam. + + New attendance + New attendance + + + %1$d new attendance + %1$d attendance + + + %d attendance + %d attendance + @@ -244,10 +282,7 @@ Only unread Only with attachments Read: %s - - Read by: %1$d of %2$d people - Read by: %1$d of %2$d people - + Read by: %1$d of %2$d people %d message %d messages @@ -313,6 +348,9 @@ No info about homework Mark as done Mark as undone + Add homework + Homework added successfully + Homework deleted successfully Attachments New homework @@ -336,10 +374,12 @@ Today\'s lucky number is: %s Show history + Lucky number history No info about lucky numbers + Mobile devices No devices @@ -388,7 +428,8 @@ You have %1$d new conference You have %1$d new conferences - + Present at conference + Agenda School announcements @@ -495,6 +536,7 @@ Lessons (Tomorrow) + (Today and tomorrow) In a moment: Soon: First: @@ -585,6 +627,11 @@ Yes No Save + Title + Add + Copied + Undo + Change @@ -596,13 +643,13 @@ - App appearance & behavior + App Default view - Calculation of the end-of-year average + Calculated average options Force average calculation by app Show presence Theme - Expand grades + Grades expanding Mark current lesson Show groups next to subjects Show chart list in class grades @@ -612,14 +659,21 @@ Language Notifications + Other Show notifications Show upcoming lesson notifications + Make upcoming lesson notification persistent + Turn off when notification is not showing in your watch/band Open system notification settings Fix synchronization & notifications issues Your device may have data synchronization issues and with notifications.\n\nTo fix them, you need to add Wulkanowy to the autostart and turn off battery optimization/saving in the phone settings. Go to settings Show debug notifications Synchronization is disabled + Capture official app notifications + Capture notifications + With this feature you can gain a substitute of push notifications like in the official app. All you need to do is allow Wulkanowy to receive all notifications in your system settings.\n\nHow it works?\nWhen you get a notification in Dziennik VULCAN, Wulkanowy will be notified (that\'s what these extra permissions are for) and will trigger a sync so that can send its own notification.\n\nFOR ADVANCED USERS ONLY + Go to settings Synchronization Automatic update @@ -637,10 +691,19 @@ Reply with message history Show arithmetic average when no weights provided + Support + Watch single ad to support project + Consent to data processing + To view an advertisement you must agree to the data processing terms of our Privacy Policy + Agree + Privacy policy + Ad is loading + Advanced Appearance & Behavior Notifications Synchronization + Advertisements Grades Dashboard @@ -648,6 +711,7 @@ Attendance Timetable Grades + Calculated average Messages Appearance & Behavior @@ -658,7 +722,8 @@ Automatic update, synchronization interval Plus and minus values, average calculation Advanced - App version, contributors, social portals, licenses + App version, contributors, social portals + Displaying advertisements, project support @@ -673,6 +738,8 @@ Push notifications Upcoming lessons Debug + Timetable change + New attendance @@ -684,12 +751,6 @@ No color - - Copied - Undo - Change - - Download of updates has started… An update has just been downloaded. @@ -708,4 +769,5 @@ An unexpected error occurred Feature disabled by your school Feature not available. Login in a mode other than Mobile API + This field is required diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 628aa297..7cd0f725 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -11,6 +11,7 @@ @color/colorError @color/colorDivider @color/colorSwipeRefresh + @color/dashboard_message_medium_dark @android:color/darker_gray ?android:textColorPrimary @style/PreferenceThemeOverlay @@ -22,10 +23,11 @@ true - @@ -49,8 +50,6 @@ 11sp -