diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..35fbd4661 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +[*] +charset=utf-8 +end_of_line=lf +insert_final_newline=true +indent_style=space +indent_size=4 + +[*.json] +indent_size=2 + +[*.{kt,kts}] +disabled_rules=import-ordering,no-wildcard-imports diff --git a/.github/workflows/deploy-store.yml b/.github/workflows/deploy-store.yml index 12338feff..3ce618ca7 100644 --- a/.github/workflows/deploy-store.yml +++ b/.github/workflows/deploy-store.yml @@ -1,4 +1,4 @@ -name: Deploy to app stores +name: Deploy release on: release: @@ -7,16 +7,17 @@ on: jobs: deploy-google-play: - name: Deploy to google play + name: Google Play runs-on: ubuntu-latest timeout-minutes: 10 environment: google-play steps: - - uses: actions/checkout@v2 - - uses: actions/setup-java@v1 + - uses: actions/checkout@v3 + - uses: actions/setup-java@v2 with: + distribution: 'zulu' java-version: 11 - - uses: actions/cache@v2 + - uses: actions/cache@v3 with: path: | ~/.gradle/caches @@ -37,20 +38,22 @@ jobs: ANDROID_PUBLISHER_CREDENTIALS: ${{ secrets.ANDROID_PUBLISHER_CREDENTIALS }} ADMOB_PROJECT_ID: ${{ secrets.ADMOB_PROJECT_ID }} SINGLE_SUPPORT_AD_ID: ${{ secrets.SINGLE_SUPPORT_AD_ID }} + DASHBOARD_TILE_AD_ID: ${{ secrets.DASHBOARD_TILE_AD_ID }} SET_BUILD_TIMESTAMP: ${{ secrets.SET_BUILD_TIMESTAMP }} run: ./gradlew publishPlayReleaseApps -PenableFirebase --stacktrace; deploy-app-gallery: - name: Deploy to AppGallery + name: AppGallery runs-on: ubuntu-latest timeout-minutes: 10 environment: app-gallery steps: - - uses: actions/checkout@v2 - - uses: actions/setup-java@v1 + - uses: actions/checkout@v3 + - uses: actions/setup-java@v2 with: + distribution: 'zulu' java-version: 11 - - uses: actions/cache@v2 + - uses: actions/cache@v3 with: path: | ~/.gradle/caches diff --git a/.github/workflows/deploy-test.yml b/.github/workflows/deploy-test.yml index 88edca05d..20082590d 100644 --- a/.github/workflows/deploy-test.yml +++ b/.github/workflows/deploy-test.yml @@ -1,4 +1,4 @@ -name: Deploy to app tests +name: Deploy DEV on: push: @@ -18,11 +18,12 @@ jobs: timeout-minutes: 10 environment: app-center steps: - - uses: actions/checkout@v2 - - uses: actions/setup-java@v1 + - uses: actions/checkout@v3 + - uses: actions/setup-java@v2 with: + distribution: 'zulu' java-version: 11 - - uses: actions/cache@v2 + - uses: actions/cache@v3 with: path: | ~/.gradle/caches @@ -66,7 +67,7 @@ jobs: BITRISE_KEY_PASSWORD: ${{ secrets.BITRISE_KEY_PASSWORD }} run: ./gradlew assembleFdroidDebug --stacktrace - name: Upload apk to github artifacts - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: wulkanowyDEV-${{ env.RUN_NUMBER }}.apk path: app/build/outputs/apk/fdroid/debug/app-fdroid-debug.apk @@ -87,11 +88,12 @@ jobs: environment: app-distribution if: github.event_name != 'pull_request_target' steps: - - uses: actions/checkout@v2 - - uses: actions/setup-java@v1 + - uses: actions/checkout@v3 + - uses: actions/setup-java@v2 with: + distribution: 'zulu' java-version: 11 - - uses: actions/cache@v2 + - uses: actions/cache@v3 with: path: | ~/.gradle/caches @@ -131,7 +133,7 @@ jobs: BITRISE_KEY_PASSWORD: ${{ secrets.BITRISE_KEY_PASSWORD }} run: ./gradlew assemblePlayDebug -PenableFirebase --stacktrace - name: Upload apk to github artifacts - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: wulkanowyDEV-${{ env.RUN_NUMBER }}-dev.apk path: app/build/outputs/apk/play/debug/app-play-debug.apk diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ee16041f6..3def08953 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,18 +8,20 @@ on: branches: [ master, develop ] jobs: - unit-tests: - name: Unit tests + + tests-fdroid: + name: F-Droid runs-on: ubuntu-latest timeout-minutes: 10 steps: - uses: fkirc/skip-duplicate-actions@master - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: gradle/wrapper-validation-action@v1 - - uses: actions/setup-java@v1 + - uses: actions/setup-java@v2 with: + distribution: 'zulu' java-version: 11 - - uses: actions/cache@v2 + - uses: actions/cache@v3 with: path: | ~/.gradle/caches @@ -29,6 +31,58 @@ jobs: run: | ./gradlew testFdroidDebugUnitTest --stacktrace ./gradlew jacocoTestReport --stacktrace - - uses: codecov/codecov-action@v1 + - uses: codecov/codecov-action@v3 + with: + flags: unit + + tests-play: + name: Play + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: fkirc/skip-duplicate-actions@master + - uses: actions/checkout@v3 + - uses: gradle/wrapper-validation-action@v1 + - uses: actions/setup-java@v2 + with: + distribution: 'zulu' + java-version: 11 + - uses: actions/cache@v3 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*') }} + - name: Unit tests + run: | + ./gradlew testPlayDebugUnitTest --stacktrace + ./gradlew jacocoTestReport --stacktrace + - uses: codecov/codecov-action@v3 + with: + flags: unit + + tests-hms: + name: HMS + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: fkirc/skip-duplicate-actions@master + - uses: actions/checkout@v3 + - uses: gradle/wrapper-validation-action@v1 + - uses: actions/setup-java@v2 + with: + distribution: 'zulu' + java-version: 11 + - uses: actions/cache@v3 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*') }} + - name: Unit tests + run: | + ./gradlew testHmsDebugUnitTest --stacktrace + ./gradlew jacocoTestReport --stacktrace + - uses: codecov/codecov-action@v3 with: flags: unit diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index ab7844747..1f93faefd 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -2,14 +2,6 @@ \ No newline at end of file diff --git a/LICENSE b/LICENSE index 2fb96cee8..c97032f74 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2021 Wulkanowy + Copyright 2022 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/README.de.md b/README.de.md index 3f806e9fd..b9e1d1ec1 100644 --- a/README.de.md +++ b/README.de.md @@ -51,7 +51,7 @@ Die aktuelle Version können Sie von der Google Play, F-Droid oder Huawei AppGal alt="Explore it on AppGallery" height="80">](https://appgallery.cloud.huawei.com/ag/n/app/C101440411?channelId=Badge&id=1b3f7fbb700849a9be0dba6b520b2282&s=EB1D3BF9ED9D1564D869B7B94B18016D3CABFCA5AEFB8E29F675FA04E0DC131D&detailType=0&v=) -Sie können auch ein [Entwicklungsversion herunterladen](https://wulkanowy.github.io/#download) das beinhaltet neue Funktionen, die für die nächste Version vorbereitet werden +Sie können auch eine [Entwicklungsversion herunterladen](https://wulkanowy.github.io/#download) die beinhaltet neue Funktionen, die für die nächste Version vorbereitet werden ## Gebaut mit diff --git a/app/build.gradle b/app/build.gradle index ea74cd562..6584b0d28 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -15,33 +15,35 @@ apply from: 'sonarqube.gradle' apply from: 'hooks.gradle' android { - compileSdkVersion 31 + namespace 'io.github.wulkanowy' + compileSdkVersion 32 defaultConfig { applicationId "io.github.wulkanowy" testApplicationId "io.github.tests.wulkanowy" minSdkVersion 21 - targetSdkVersion 31 - versionCode 102 - versionName "1.4.4" + targetSdkVersion 32 + versionCode 114 + versionName "1.7.5" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" resValue "string", "app_name", "Wulkanowy" manifestPlaceholders = [ - firebase_enabled: project.hasProperty("enableFirebase"), - admob_project_id: "" + firebase_enabled: project.hasProperty("enableFirebase"), + admob_project_id: "" ] javaCompileOptions { annotationProcessorOptions { arguments += [ - "room.schemaLocation": "$projectDir/schemas".toString(), - "room.incremental" : "true" + "room.schemaLocation": "$projectDir/schemas".toString(), + "room.incremental" : "true" ] } } buildConfigField "String", "SINGLE_SUPPORT_AD_ID", "null" + buildConfigField "String", "DASHBOARD_TILE_AD_ID", "null" if (System.env.SET_BUILD_TIMESTAMP) { buildConfigField "long", "BUILD_TIMESTAMP", String.valueOf(System.currentTimeMillis()) @@ -73,6 +75,8 @@ android { buildConfigField "String", "MESSAGES_BASE_URL", "\"https://messages.wulkanowy.net.pl\"" } debug { + minifyEnabled false + shrinkResources false resValue "string", "app_name", "Wulkanowy DEV" applicationIdSuffix ".dev" versionNameSuffix "-dev" @@ -92,10 +96,12 @@ android { play { dimension "platform" manifestPlaceholders = [ - install_channel : "Google Play", - admob_project_id: System.getenv("ADMOB_PROJECT_ID") ?: "ca-app-pub-3940256099942544~3347511713" + 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"}\"" + buildConfigField "String", "DASHBOARD_TILE_AD_ID", "\"${System.getenv("DASHBOARD_TILE_AD_ID") ?: "ca-app-pub-3940256099942544/6300978111"}\"" + } fdroid { @@ -120,6 +126,8 @@ android { testOptions.unitTests { includeAndroidResources = true + // workaround HMS test errors https://github.com/robolectric/robolectric/issues/2750 + all { jvmArgs '-noverify' } } compileOptions { @@ -130,12 +138,14 @@ android { kotlinOptions { jvmTarget = "11" - freeCompilerArgs += ["-Xopt-in=kotlin.RequiresOptIn", "-Xjvm-default=all"] + freeCompilerArgs += ["-opt-in=kotlin.RequiresOptIn", "-Xjvm-default=all"] } packagingOptions { - exclude 'META-INF/library_release.kotlin_module' - exclude 'META-INF/library-core_release.kotlin_module' + resources { + excludes += ['META-INF/library_release.kotlin_module', + 'META-INF/library-core_release.kotlin_module'] + } } aboutLibraries { @@ -149,7 +159,9 @@ kapt { play { defaultToAppBundles = false - track = 'beta' + track = 'production' +// releaseStatus = com.github.triplet.gradle.androidpublisher.ReleaseStatus.IN_PROGRESS +// userFraction = 0.05d updatePriority = 4 enabled.set(false) } @@ -167,34 +179,34 @@ huaweiPublish { ext { work_manager = "2.7.1" android_hilt = "1.0.0" - room = "2.3.0" + room = "2.4.3" chucker = "3.5.2" - mockk = "1.12.1" - coroutines = "1.5.2" + mockk = "1.12.7" + coroutines = "1.6.4" } dependencies { - implementation "io.github.wulkanowy:sdk:1.4.4" + implementation "io.github.wulkanowy:sdk:1.7.5" - coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.8' - implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.1" + implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.0" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines" - 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" - implementation "androidx.fragment:fragment-ktx:1.4.0" - implementation "androidx.annotation:annotation:1.3.0" + implementation "androidx.core:core-ktx:1.8.0" + implementation 'androidx.core:core-splashscreen:1.0.0' + implementation "androidx.activity:activity-ktx:1.5.1" + implementation "androidx.appcompat:appcompat:1.5.0" + implementation "androidx.fragment:fragment-ktx:1.5.2" + implementation "androidx.annotation:annotation:1.4.0" - implementation "androidx.preference:preference-ktx:1.1.1" + implementation "androidx.preference:preference-ktx:1.2.0" implementation "androidx.recyclerview:recyclerview:1.2.1" implementation "androidx.viewpager2:viewpager2:1.1.0-beta01" implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" - implementation "androidx.constraintlayout:constraintlayout:2.1.2" - implementation "androidx.coordinatorlayout:coordinatorlayout:1.1.0" - implementation "com.google.android.material:material:1.4.0" + implementation "androidx.constraintlayout:constraintlayout:2.1.4" + implementation "androidx.coordinatorlayout:coordinatorlayout:1.2.0" + implementation "com.google.android.material:material:1.6.1" 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' @@ -202,7 +214,7 @@ dependencies { implementation "androidx.work:work-runtime-ktx:$work_manager" playImplementation "androidx.work:work-gcm:$work_manager" - implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.0" + implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.5.1" implementation "androidx.room:room-runtime:$room" implementation "androidx.room:room-ktx:$room" @@ -218,39 +230,40 @@ dependencies { 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.3" + implementation "com.squareup.okhttp3:logging-interceptor:4.10.0" implementation "com.jakewharton.timber:timber:5.0.1" implementation "at.favre.lib:slf4j-timber:1.0.1" implementation 'com.github.bastienpaulfr:Treessence:1.0.5' implementation "com.mikepenz:aboutlibraries-core:$about_libraries" - implementation "io.coil-kt:coil:1.4.0" + implementation "io.coil-kt:coil:2.2.0" implementation "io.github.wulkanowy:AppKillerManager:3.0.0" - implementation 'me.xdrop:fuzzywuzzy:1.3.1' - implementation 'com.fredporciuncula:flow-preferences:1.6.0' + implementation 'me.xdrop:fuzzywuzzy:1.4.0' + implementation 'com.fredporciuncula:flow-preferences:1.8.0' - playImplementation platform('com.google.firebase:firebase-bom:29.0.2') + playImplementation platform('com.google.firebase:firebase-bom:30.3.2') 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.2' + playImplementation 'com.google.android.play:core:1.10.3' playImplementation 'com.google.android.play:core-ktx:1.8.1' - playImplementation 'com.google.android.gms:play-services-ads:20.5.0' + playImplementation 'com.google.android.gms:play-services-ads:21.1.0' - hmsImplementation 'com.huawei.hms:hianalytics:6.3.2.300' - hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.6.2.300' + hmsImplementation 'com.huawei.hms:hianalytics:6.7.0.300' + hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.7.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:1.0.6' + debugImplementation 'com.github.haroldadmin:WhatTheStack:1.0.0-alpha04' testImplementation "junit:junit:4.13.2" testImplementation "io.mockk:mockk:$mockk" testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines" testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version" - testImplementation 'org.robolectric:robolectric:4.7.3' + testImplementation 'org.robolectric:robolectric:4.8.2' 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/schemas/io.github.wulkanowy.data.db.AppDatabase/45.json b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/45.json new file mode 100644 index 000000000..57f3d431d --- /dev/null +++ b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/45.json @@ -0,0 +1,2430 @@ +{ + "formatVersion": 1, + "database": { + "version": 45, + "identityHash": "f310243440ca00cbc35e62ebaca5c7d8", + "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" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Students_email_symbol_student_id_school_id_class_id` ON `${TABLE_NAME}` (`email`, `symbol`, `student_id`, `school_id`, `class_id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "Semesters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `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" + ], + "orders": [], + "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, `repeat_id` BLOB DEFAULT NULL, `is_added_by_user` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repeatId", + "columnName": "repeat_id", + "affinity": "BLOB", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "isAddedByUser", + "columnName": "is_added_by_user", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "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, `is_dismissible` INTEGER 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 + }, + { + "fieldPath": "isDismissible", + "columnName": "is_dismissible", + "affinity": "INTEGER", + "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, 'f310243440ca00cbc35e62ebaca5c7d8')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.github.wulkanowy.data.db.AppDatabase/46.json b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/46.json new file mode 100644 index 000000000..04518141c --- /dev/null +++ b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/46.json @@ -0,0 +1,2430 @@ +{ + "formatVersion": 1, + "database": { + "version": 46, + "identityHash": "f310243440ca00cbc35e62ebaca5c7d8", + "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" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Students_email_symbol_student_id_school_id_class_id` ON `${TABLE_NAME}` (`email`, `symbol`, `student_id`, `school_id`, `class_id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "Semesters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `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" + ], + "orders": [], + "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, `repeat_id` BLOB DEFAULT NULL, `is_added_by_user` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repeatId", + "columnName": "repeat_id", + "affinity": "BLOB", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "isAddedByUser", + "columnName": "is_added_by_user", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "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, `is_dismissible` INTEGER 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 + }, + { + "fieldPath": "isDismissible", + "columnName": "is_dismissible", + "affinity": "INTEGER", + "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, 'f310243440ca00cbc35e62ebaca5c7d8')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.github.wulkanowy.data.db.AppDatabase/47.json b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/47.json new file mode 100644 index 000000000..3f8291eac --- /dev/null +++ b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/47.json @@ -0,0 +1,2438 @@ +{ + "formatVersion": 1, + "database": { + "version": 47, + "identityHash": "ac88c80d4bb923b22f22ce4f91521306", + "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" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Students_email_symbol_student_id_school_id_class_id` ON `${TABLE_NAME}` (`email`, `symbol`, `student_id`, `school_id`, `class_id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "Semesters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `kindergarten_diary_id` INTEGER NOT NULL DEFAULT 0, `diary_name` TEXT NOT NULL, `school_year` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `semester_name` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `unit_id` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_current` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "kindergartenDiaryId", + "columnName": "kindergarten_diary_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "diaryName", + "columnName": "diary_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolYear", + "columnName": "school_year", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterName", + "columnName": "semester_name", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unitId", + "columnName": "unit_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "current", + "columnName": "is_current", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_Semesters_student_id_diary_id_kindergarten_diary_id_semester_id", + "unique": true, + "columnNames": [ + "student_id", + "diary_id", + "kindergarten_diary_id", + "semester_id" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Semesters_student_id_diary_id_kindergarten_diary_id_semester_id` ON `${TABLE_NAME}` (`student_id`, `diary_id`, `kindergarten_diary_id`, `semester_id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "Exams", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `entry_date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `group` TEXT NOT NULL, `type` TEXT NOT NULL, `description` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "entryDate", + "columnName": "entry_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "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, `repeat_id` BLOB DEFAULT NULL, `is_added_by_user` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repeatId", + "columnName": "repeat_id", + "affinity": "BLOB", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "isAddedByUser", + "columnName": "is_added_by_user", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "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, `is_dismissible` INTEGER 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 + }, + { + "fieldPath": "isDismissible", + "columnName": "is_dismissible", + "affinity": "INTEGER", + "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, 'ac88c80d4bb923b22f22ce4f91521306')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.github.wulkanowy.data.db.AppDatabase/48.json b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/48.json new file mode 100644 index 000000000..1c11aae91 --- /dev/null +++ b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/48.json @@ -0,0 +1,2445 @@ +{ + "formatVersion": 1, + "database": { + "version": 48, + "identityHash": "95751b933ad9f835ffc1805f4ef71bdb", + "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" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Students_email_symbol_student_id_school_id_class_id` ON `${TABLE_NAME}` (`email`, `symbol`, `student_id`, `school_id`, `class_id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "Semesters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `kindergarten_diary_id` INTEGER NOT NULL DEFAULT 0, `diary_name` TEXT NOT NULL, `school_year` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `semester_name` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `unit_id` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_current` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "kindergartenDiaryId", + "columnName": "kindergarten_diary_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "diaryName", + "columnName": "diary_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolYear", + "columnName": "school_year", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterName", + "columnName": "semester_name", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unitId", + "columnName": "unit_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "current", + "columnName": "is_current", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_Semesters_student_id_diary_id_kindergarten_diary_id_semester_id", + "unique": true, + "columnNames": [ + "student_id", + "diary_id", + "kindergarten_diary_id", + "semester_id" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Semesters_student_id_diary_id_kindergarten_diary_id_semester_id` ON `${TABLE_NAME}` (`student_id`, `diary_id`, `kindergarten_diary_id`, `semester_id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "Exams", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `entry_date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `group` TEXT NOT NULL, `type` TEXT NOT NULL, `description` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "entryDate", + "columnName": "entry_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "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, `repeat_id` BLOB DEFAULT NULL, `is_added_by_user` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repeatId", + "columnName": "repeat_id", + "affinity": "BLOB", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "isAddedByUser", + "columnName": "is_added_by_user", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "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, `destination` TEXT NOT NULL DEFAULT '{\"type\":\"io.github.wulkanowy.ui.modules.Destination.Dashboard\"}', `date` INTEGER NOT NULL, `data` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "destination", + "columnName": "destination", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'{\"type\":\"io.github.wulkanowy.ui.modules.Destination.Dashboard\"}'" + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "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, `is_dismissible` INTEGER 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 + }, + { + "fieldPath": "isDismissible", + "columnName": "is_dismissible", + "affinity": "INTEGER", + "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, '95751b933ad9f835ffc1805f4ef71bdb')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.github.wulkanowy.data.db.AppDatabase/49.json b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/49.json new file mode 100644 index 000000000..5472fb78a --- /dev/null +++ b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/49.json @@ -0,0 +1,2445 @@ +{ + "formatVersion": 1, + "database": { + "version": 49, + "identityHash": "790d4dc0e11f38349c49af85fabf9b7b", + "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" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Students_email_symbol_student_id_school_id_class_id` ON `${TABLE_NAME}` (`email`, `symbol`, `student_id`, `school_id`, `class_id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "Semesters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `kindergarten_diary_id` INTEGER NOT NULL DEFAULT 0, `diary_name` TEXT NOT NULL, `school_year` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `semester_name` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `unit_id` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_current` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "kindergartenDiaryId", + "columnName": "kindergarten_diary_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "diaryName", + "columnName": "diary_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolYear", + "columnName": "school_year", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterName", + "columnName": "semester_name", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unitId", + "columnName": "unit_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "current", + "columnName": "is_current", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_Semesters_student_id_diary_id_kindergarten_diary_id_semester_id", + "unique": true, + "columnNames": [ + "student_id", + "diary_id", + "kindergarten_diary_id", + "semester_id" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Semesters_student_id_diary_id_kindergarten_diary_id_semester_id` ON `${TABLE_NAME}` (`student_id`, `diary_id`, `kindergarten_diary_id`, `semester_id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "Exams", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `entry_date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `group` TEXT NOT NULL, `type` TEXT NOT NULL, `description` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "entryDate", + "columnName": "entry_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "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, `repeat_id` BLOB DEFAULT NULL, `is_added_by_user` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repeatId", + "columnName": "repeat_id", + "affinity": "BLOB", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "isAddedByUser", + "columnName": "is_added_by_user", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "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}` (`user_login_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `content` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "userLoginId", + "columnName": "user_login_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "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, `destination` TEXT NOT NULL DEFAULT '{\"type\":\"io.github.wulkanowy.ui.modules.Destination.Dashboard\"}', `date` INTEGER NOT NULL, `data` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "destination", + "columnName": "destination", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'{\"type\":\"io.github.wulkanowy.ui.modules.Destination.Dashboard\"}'" + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "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, `is_dismissible` INTEGER 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 + }, + { + "fieldPath": "isDismissible", + "columnName": "is_dismissible", + "affinity": "INTEGER", + "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, '790d4dc0e11f38349c49af85fabf9b7b')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.github.wulkanowy.data.db.AppDatabase/50.json b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/50.json new file mode 100644 index 000000000..4361db954 --- /dev/null +++ b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/50.json @@ -0,0 +1,2445 @@ +{ + "formatVersion": 1, + "database": { + "version": 50, + "identityHash": "87455aae2b15baa976386c833afa9cd9", + "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" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Students_email_symbol_student_id_school_id_class_id` ON `${TABLE_NAME}` (`email`, `symbol`, `student_id`, `school_id`, `class_id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "Semesters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `kindergarten_diary_id` INTEGER NOT NULL DEFAULT 0, `diary_name` TEXT NOT NULL, `school_year` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `semester_name` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `unit_id` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_current` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "kindergartenDiaryId", + "columnName": "kindergarten_diary_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "diaryName", + "columnName": "diary_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolYear", + "columnName": "school_year", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterName", + "columnName": "semester_name", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unitId", + "columnName": "unit_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "current", + "columnName": "is_current", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_Semesters_student_id_diary_id_kindergarten_diary_id_semester_id", + "unique": true, + "columnNames": [ + "student_id", + "diary_id", + "kindergarten_diary_id", + "semester_id" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Semesters_student_id_diary_id_kindergarten_diary_id_semester_id` ON `${TABLE_NAME}` (`student_id`, `diary_id`, `kindergarten_diary_id`, `semester_id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "Exams", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `entry_date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `group` TEXT NOT NULL, `type` TEXT NOT NULL, `description` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "entryDate", + "columnName": "entry_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "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}` (`user_login_id` INTEGER NOT NULL, `device_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `date` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "userLoginId", + "columnName": "user_login_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deviceId", + "columnName": "device_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "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, `repeat_id` BLOB DEFAULT NULL, `is_added_by_user` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repeatId", + "columnName": "repeat_id", + "affinity": "BLOB", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "isAddedByUser", + "columnName": "is_added_by_user", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "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}` (`user_login_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `content` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "userLoginId", + "columnName": "user_login_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "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, `destination` TEXT NOT NULL DEFAULT '{\"type\":\"io.github.wulkanowy.ui.modules.Destination.Dashboard\"}', `date` INTEGER NOT NULL, `data` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "destination", + "columnName": "destination", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'{\"type\":\"io.github.wulkanowy.ui.modules.Destination.Dashboard\"}'" + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "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, `is_dismissible` INTEGER 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 + }, + { + "fieldPath": "isDismissible", + "columnName": "is_dismissible", + "affinity": "INTEGER", + "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, '87455aae2b15baa976386c833afa9cd9')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.github.wulkanowy.data.db.AppDatabase/51.json b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/51.json new file mode 100644 index 000000000..271b8c90b --- /dev/null +++ b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/51.json @@ -0,0 +1,2409 @@ +{ + "formatVersion": 1, + "database": { + "version": 51, + "identityHash": "51f9cb1d80df003c03bb655c0162487c", + "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" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Students_email_symbol_student_id_school_id_class_id` ON `${TABLE_NAME}` (`email`, `symbol`, `student_id`, `school_id`, `class_id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "Semesters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `kindergarten_diary_id` INTEGER NOT NULL DEFAULT 0, `diary_name` TEXT NOT NULL, `school_year` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `semester_name` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `unit_id` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_current` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "kindergartenDiaryId", + "columnName": "kindergarten_diary_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "diaryName", + "columnName": "diary_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolYear", + "columnName": "school_year", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterName", + "columnName": "semester_name", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unitId", + "columnName": "unit_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "current", + "columnName": "is_current", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_Semesters_student_id_diary_id_kindergarten_diary_id_semester_id", + "unique": true, + "columnNames": [ + "student_id", + "diary_id", + "kindergarten_diary_id", + "semester_id" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Semesters_student_id_diary_id_kindergarten_diary_id_semester_id` ON `${TABLE_NAME}` (`student_id`, `diary_id`, `kindergarten_diary_id`, `semester_id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "Exams", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `entry_date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `group` TEXT NOT NULL, `type` TEXT NOT NULL, `description` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "entryDate", + "columnName": "entry_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "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}` (`message_global_key` TEXT NOT NULL, `mailbox_key` TEXT NOT NULL, `message_id` INTEGER NOT NULL, `correspondents` TEXT NOT NULL, `subject` TEXT NOT NULL, `date` INTEGER NOT NULL, `folder_id` INTEGER NOT NULL, `unread` INTEGER NOT NULL, `has_attachments` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL, `content` TEXT NOT NULL, `sender` TEXT, `recipients` TEXT)", + "fields": [ + { + "fieldPath": "messageGlobalKey", + "columnName": "message_global_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mailboxKey", + "columnName": "mailbox_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "messageId", + "columnName": "message_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "correspondents", + "columnName": "correspondents", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "folderId", + "columnName": "folder_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasAttachments", + "columnName": "has_attachments", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sender", + "columnName": "sender", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "recipients", + "columnName": "recipients", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MessageAttachments", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`real_id` INTEGER NOT NULL, `message_global_key` 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": "messageGlobalKey", + "columnName": "message_global_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "filename", + "columnName": "filename", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "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": "Mailboxes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`globalKey` TEXT NOT NULL, `fullName` TEXT NOT NULL, `userName` TEXT NOT NULL, `userLoginId` INTEGER NOT NULL, `studentName` TEXT NOT NULL, `schoolNameShort` TEXT NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`globalKey`))", + "fields": [ + { + "fieldPath": "globalKey", + "columnName": "globalKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fullName", + "columnName": "fullName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userName", + "columnName": "userName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userLoginId", + "columnName": "userLoginId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentName", + "columnName": "studentName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolNameShort", + "columnName": "schoolNameShort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "globalKey" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Recipients", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`mailboxGlobalKey` TEXT NOT NULL, `studentMailboxGlobalKey` TEXT NOT NULL, `fullName` TEXT NOT NULL, `userName` TEXT NOT NULL, `schoolShortName` TEXT NOT NULL, `type` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "mailboxGlobalKey", + "columnName": "mailboxGlobalKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentMailboxGlobalKey", + "columnName": "studentMailboxGlobalKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fullName", + "columnName": "fullName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userName", + "columnName": "userName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolShortName", + "columnName": "schoolShortName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MobileDevices", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_login_id` INTEGER NOT NULL, `device_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `date` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "userLoginId", + "columnName": "user_login_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deviceId", + "columnName": "device_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "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, `repeat_id` BLOB DEFAULT NULL, `is_added_by_user` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repeatId", + "columnName": "repeat_id", + "affinity": "BLOB", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "isAddedByUser", + "columnName": "is_added_by_user", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "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}` (`user_login_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `content` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "userLoginId", + "columnName": "user_login_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "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, `destination` TEXT NOT NULL DEFAULT '{\"type\":\"io.github.wulkanowy.ui.modules.Destination.Dashboard\"}', `date` INTEGER NOT NULL, `data` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "destination", + "columnName": "destination", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'{\"type\":\"io.github.wulkanowy.ui.modules.Destination.Dashboard\"}'" + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "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, `is_dismissible` INTEGER 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 + }, + { + "fieldPath": "isDismissible", + "columnName": "is_dismissible", + "affinity": "INTEGER", + "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, '51f9cb1d80df003c03bb655c0162487c')" + ] + } +} \ No newline at end of file diff --git a/app/src/debug/agconnect-services.json b/app/src/debug/agconnect-services.json index 48192df01..52426f54e 100644 --- a/app/src/debug/agconnect-services.json +++ b/app/src/debug/agconnect-services.json @@ -1,33 +1,92 @@ { - "agcgw":{ - "backurl":"connect-dre.dbankcloud.cn", - "url":"connect-dre.hispace.hicloud.com" - }, - "client":{ - "cp_id":"890048000024105546", - "product_id":"", - "client_id":"", - "client_secret":"", - "app_id":"101440411", - "package_name":"io.github.wulkanowy.dev", - "api_key":"" - }, - "service":{ - "analytics":{ - "collector_url":"datacollector-dre.dt.hicloud.com,datacollector-dre.dt.dbankcloud.cn", - "resource_id":"p1", - "channel_id":"" - }, + "agcgw": { + "backurl": "connect-dre.hispace.hicloud.com", + "url": "connect-dre.dbankcloud.cn", + "websocketbackurl": "connect-ws-dre.hispace.dbankcloud.com", + "websocketurl": "connect-ws-dre.hispace.dbankcloud.cn" + }, + "agcgw_all": { + "CN": "connect-drcn.dbankcloud.cn", + "CN_back": "connect-drcn.hispace.hicloud.com", + "DE": "connect-dre.dbankcloud.cn", + "DE_back": "connect-dre.hispace.hicloud.com", + "RU": "connect-drru.hispace.dbankcloud.ru", + "RU_back": "connect-drru.hispace.dbankcloud.cn", + "SG": "connect-dra.dbankcloud.cn", + "SG_back": "connect-dra.hispace.hicloud.com" + }, + "websocketgw_all": { + "CN": "connect-ws-drcn.hispace.dbankcloud.cn", + "CN_back": "connect-ws-drcn.hispace.dbankcloud.com", + "DE": "connect-ws-dre.hispace.dbankcloud.cn", + "DE_back": "connect-ws-dre.hispace.dbankcloud.com", + "RU": "connect-ws-drru.hispace.dbankcloud.ru", + "RU_back": "connect-ws-drru.hispace.dbankcloud.cn", + "SG": "connect-ws-dra.hispace.dbankcloud.cn", + "SG_back": "connect-ws-dra.hispace.dbankcloud.com" + }, + "client": { + "cp_id": "890048000024105546", + "product_id": "736430079244736562", + "client_id": "514530959291319360", + "client_secret": "C42522DBF17D3D4BBE9D9C1783A54484B7E6844B388B7A67502D36A633A4186B", + "project_id": "736430079244736562", + "app_id": "106552551", + "api_key": "CgB6e3x9BUNiq+r8ebCHNojjjYsMT4pJSjjNDOkm9owtBb6rVI6LjnASoZBRxbjjhObcrV5gANo99fI/eKZDTbWS", + "package_name": "io.github.wulkanowy.dev" + }, + "oauth_client": { + "client_id": "106552551", + "client_type": 1 + }, + "app_info": { + "app_id": "106552551", + "package_name": "io.github.wulkanowy.dev" + }, + "service": { + "analytics": { + "collector_url": "datacollector-dre.dt.hicloud.com,datacollector-dre.dt.dbankcloud.cn", + "collector_url_ru": "datacollector-drru.dt.dbankcloud.ru,datacollector-drru.dt.hicloud.com", + "collector_url_sg": "datacollector-dra.dt.hicloud.com,datacollector-dra.dt.dbankcloud.cn", + "collector_url_de": "datacollector-dre.dt.hicloud.com,datacollector-dre.dt.dbankcloud.cn", + "collector_url_cn": "datacollector-drcn.dt.hicloud.com,datacollector-drcn.dt.dbankcloud.cn", + "resource_id": "p1", + "channel_id": "" + }, "search":{ "url":"https://search-dre.cloud.huawei.com" }, - "cloudstorage":{ - "storage_url":"https://ops-dre.agcstorage.link" - }, - "ml":{ - "mlservice_url":"ml-api-dre.ai.dbankcloud.com,ml-api-dre.ai.dbankcloud.cn" - } - }, - "region":"DE", - "configuration_version":"1.0" + "cloudstorage": { + "storage_url_sg_back": "https://agc-storage-dra.cloud.huawei.asia", + "storage_url_ru_back": "https://agc-storage-drru.cloud.huawei.ru", + "storage_url_ru": "https://agc-storage-drru.cloud.huawei.ru", + "storage_url_de_back": "https://agc-storage-dre.cloud.huawei.eu", + "storage_url_de": "https://ops-dre.agcstorage.link", + "storage_url": "https://agc-storage-drcn.platform.dbankcloud.cn", + "storage_url_sg": "https://ops-dra.agcstorage.link", + "storage_url_cn_back": "https://agc-storage-drcn.cloud.huawei.com.cn", + "storage_url_cn": "https://agc-storage-drcn.platform.dbankcloud.cn" + }, + "ml": { + "mlservice_url": "ml-api-dre.ai.dbankcloud.com,ml-api-dre.ai.dbankcloud.cn" + } + }, + "region": "DE", + "configuration_version": "3.0", + "appInfos": [ + { + "package_name": "io.github.wulkanowy.dev", + "client": { + "app_id": "106552551" + }, + "app_info": { + "package_name": "io.github.wulkanowy.dev", + "app_id": "106552551" + }, + "oauth_client": { + "client_type": 1, + "client_id": "106552551" + } + } + ] } diff --git a/app/src/fdroid/java/io/github/wulkanowy/utils/AdsHelper.kt b/app/src/fdroid/java/io/github/wulkanowy/utils/AdsHelper.kt new file mode 100644 index 000000000..461d29951 --- /dev/null +++ b/app/src/fdroid/java/io/github/wulkanowy/utils/AdsHelper.kt @@ -0,0 +1,28 @@ +package io.github.wulkanowy.utils + +import android.content.Context +import android.view.View +import dagger.hilt.android.qualifiers.ApplicationContext +import io.github.wulkanowy.data.repositories.PreferencesRepository +import io.github.wulkanowy.ui.modules.dashboard.DashboardItem +import javax.inject.Inject + +@Suppress("unused") +class AdsHelper @Inject constructor( + @ApplicationContext private val context: Context, + private val preferencesRepository: PreferencesRepository +) { + + fun initialize() { + preferencesRepository.isAdsEnabled = false + preferencesRepository.isAgreeToProcessData = false + preferencesRepository.selectedDashboardTiles -= DashboardItem.Tile.ADS + } + + @Suppress("RedundantSuspendModifier", "UNUSED_PARAMETER") + suspend fun getDashboardTileAdBanner(width: Int): AdBanner { + throw IllegalStateException("Can't get ad banner (F-droid)") + } +} + +data class AdBanner(val view: View) diff --git a/app/src/hms/java/io/github/wulkanowy/utils/AdsHelper.kt b/app/src/hms/java/io/github/wulkanowy/utils/AdsHelper.kt new file mode 100644 index 000000000..0e9227022 --- /dev/null +++ b/app/src/hms/java/io/github/wulkanowy/utils/AdsHelper.kt @@ -0,0 +1,28 @@ +package io.github.wulkanowy.utils + +import android.content.Context +import android.view.View +import dagger.hilt.android.qualifiers.ApplicationContext +import io.github.wulkanowy.data.repositories.PreferencesRepository +import io.github.wulkanowy.ui.modules.dashboard.DashboardItem +import javax.inject.Inject + +@Suppress("unused") +class AdsHelper @Inject constructor( + @ApplicationContext private val context: Context, + private val preferencesRepository: PreferencesRepository +) { + + fun initialize() { + preferencesRepository.isAdsEnabled = false + preferencesRepository.isAgreeToProcessData = false + preferencesRepository.selectedDashboardTiles -= DashboardItem.Tile.ADS + } + + @Suppress("RedundantSuspendModifier", "UNUSED_PARAMETER") + suspend fun getDashboardTileAdBanner(width: Int): AdBanner { + throw IllegalStateException("Can't get ad banner (HMS)") + } +} + +data class AdBanner(val view: View) diff --git a/app/src/hms/java/io/github/wulkanowy/utils/InAppReviewHelper.kt b/app/src/hms/java/io/github/wulkanowy/utils/InAppReviewHelper.kt index fb9bcae6c..adb162fd7 100644 --- a/app/src/hms/java/io/github/wulkanowy/utils/InAppReviewHelper.kt +++ b/app/src/hms/java/io/github/wulkanowy/utils/InAppReviewHelper.kt @@ -7,6 +7,7 @@ import javax.inject.Inject import javax.inject.Singleton @Singleton +@Suppress("UNUSED_PARAMETER", "unused") class InAppReviewHelper @Inject constructor( @ApplicationContext private val context: Context ) { @@ -14,4 +15,4 @@ class InAppReviewHelper @Inject constructor( fun showInAppReview(activity: MainActivity) { // do nothing } -} \ No newline at end of file +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index de4a80b06..7835db902 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,7 +1,6 @@ @@ -114,7 +113,6 @@ - (val status: Status, val data: T?, val error: Throwable?) { - companion object { - fun success(data: T?): Resource { - return Resource(Status.SUCCESS, data, null) - } +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import timber.log.Timber - fun error(error: Throwable?, data: T? = null): Resource { - return Resource(Status.ERROR, data, error) - } +sealed class Resource { - fun loading(data: T? = null): Resource { - return Resource(Status.LOADING, data, null) - } + open class Loading : Resource() + + data class Intermediate(val data: T) : Loading() + + data class Success(val data: T) : Resource() + + data class Error(val error: Throwable) : Resource() +} + +val Resource.dataOrNull: T? + get() = when (this) { + is Resource.Success -> this.data + is Resource.Intermediate -> this.data + is Resource.Loading -> null + is Resource.Error -> null + } + +val Resource.errorOrNull: Throwable? + get() = when (this) { + is Resource.Error -> this.error + else -> null + } + +fun resourceFlow(block: suspend () -> T) = flow { + emit(Resource.Loading()) + emit(Resource.Success(block())) +}.catch { emit(Resource.Error(it)) } + +fun flatResourceFlow(block: suspend () -> Flow>) = flow { + emit(Resource.Loading()) + emitAll(block().filter { it is Resource.Intermediate || it !is Resource.Loading }) +}.catch { emit(Resource.Error(it)) } + +fun Resource.mapData(block: (T) -> U) = when (this) { + is Resource.Success -> Resource.Success(block(this.data)) + is Resource.Intermediate -> Resource.Intermediate(block(this.data)) + is Resource.Loading -> Resource.Loading() + is Resource.Error -> Resource.Error(this.error) +} + +fun Flow>.logResourceStatus(name: String, showData: Boolean = false) = onEach { + val description = when (it) { + is Resource.Loading -> "started" + is Resource.Intermediate -> "intermediate data received" + if (showData) " (data: `${it.data}`)" else "" + is Resource.Success -> "success" + if (showData) " (data: `${it.data}`)" else "" + is Resource.Error -> "exception occurred: ${it.error}" + } + Timber.i("$name: $description") +} + +fun Flow>.mapResourceData(block: (T) -> U) = map { + it.mapData(block) +} + +fun Flow>.onResourceData(block: suspend (T) -> Unit) = onEach { + when (it) { + is Resource.Success -> block(it.data) + is Resource.Intermediate -> block(it.data) + is Resource.Error, + is Resource.Loading -> Unit } } -enum class Status { - LOADING, - SUCCESS, - ERROR +fun Flow>.onResourceLoading(block: suspend () -> Unit) = onEach { + if (it is Resource.Loading) { + block() + } +} + +fun Flow>.onResourceIntermediate(block: suspend (T) -> Unit) = onEach { + if (it is Resource.Intermediate) { + block(it.data) + } +} + +fun Flow>.onResourceSuccess(block: suspend (T) -> Unit) = onEach { + if (it is Resource.Success) { + block(it.data) + } +} + +fun Flow>.onResourceError(block: (Throwable) -> Unit) = onEach { + if (it is Resource.Error) { + block(it.error) + } +} + +fun Flow>.onResourceNotLoading(block: () -> Unit) = onEach { + if (it !is Resource.Loading) { + block() + } +} + +suspend fun Flow>.toFirstResult() = filter { it !is Resource.Loading }.first() + +suspend fun Flow>.waitForResult() = takeWhile { it is Resource.Loading }.collect() + +inline fun networkBoundResource( + mutex: Mutex = Mutex(), + showSavedOnLoading: Boolean = true, + crossinline isResultEmpty: (ResultType) -> Boolean, + crossinline query: () -> Flow, + crossinline fetch: suspend (ResultType) -> RequestType, + crossinline saveFetchResult: suspend (old: ResultType, new: RequestType) -> Unit, + crossinline onFetchFailed: (Throwable) -> Unit = { }, + crossinline shouldFetch: (ResultType) -> Boolean = { true }, + crossinline filterResult: (ResultType) -> ResultType = { it } +) = flow { + emit(Resource.Loading()) + + val data = query().first() + emitAll(if (shouldFetch(data)) { + val filteredResult = filterResult(data) + + if (showSavedOnLoading && !isResultEmpty(filteredResult)) { + emit(Resource.Intermediate(filteredResult)) + } + + try { + val newData = fetch(data) + mutex.withLock { saveFetchResult(query().first(), newData) } + query().map { Resource.Success(filterResult(it)) } + } catch (throwable: Throwable) { + onFetchFailed(throwable) + query().map { Resource.Error(throwable) } + } + } else { + query().map { Resource.Success(filterResult(it)) } + }) +} + +@JvmName("networkBoundResourceWithMap") +inline fun networkBoundResource( + mutex: Mutex = Mutex(), + showSavedOnLoading: Boolean = true, + crossinline isResultEmpty: (T) -> Boolean, + crossinline query: () -> Flow, + crossinline fetch: suspend (ResultType) -> RequestType, + crossinline saveFetchResult: suspend (old: ResultType, new: RequestType) -> Unit, + crossinline onFetchFailed: (Throwable) -> Unit = { }, + crossinline shouldFetch: (ResultType) -> Boolean = { true }, + crossinline mapResult: (ResultType) -> T +) = flow { + emit(Resource.Loading()) + + val data = query().first() + emitAll(if (shouldFetch(data)) { + val mappedResult = mapResult(data) + + if (showSavedOnLoading && !isResultEmpty(mappedResult)) { + emit(Resource.Intermediate(mappedResult)) + } + try { + val newData = fetch(data) + mutex.withLock { saveFetchResult(query().first(), newData) } + query().map { Resource.Success(mapResult(it)) } + } catch (throwable: Throwable) { + onFetchFailed(throwable) + query().map { Resource.Error(throwable) } + } + } else { + query().map { Resource.Success(mapResult(it)) } + }) } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt b/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt index d87c66299..15b38805b 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt @@ -1,114 +1,11 @@ package io.github.wulkanowy.data.db import android.content.Context -import androidx.room.Database -import androidx.room.Room -import androidx.room.RoomDatabase +import androidx.room.* 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.ExamDao -import io.github.wulkanowy.data.db.dao.GradeDao -import io.github.wulkanowy.data.db.dao.GradePartialStatisticsDao -import io.github.wulkanowy.data.db.dao.GradePointsStatisticsDao -import io.github.wulkanowy.data.db.dao.GradeSemesterStatisticsDao -import io.github.wulkanowy.data.db.dao.GradeSummaryDao -import io.github.wulkanowy.data.db.dao.HomeworkDao -import io.github.wulkanowy.data.db.dao.LuckyNumberDao -import io.github.wulkanowy.data.db.dao.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 -import io.github.wulkanowy.data.db.dao.StudentInfoDao -import io.github.wulkanowy.data.db.dao.SubjectDao -import io.github.wulkanowy.data.db.dao.TeacherDao -import io.github.wulkanowy.data.db.dao.TimetableAdditionalDao -import io.github.wulkanowy.data.db.dao.TimetableDao -import io.github.wulkanowy.data.db.dao.TimetableHeaderDao -import io.github.wulkanowy.data.db.entities.AdminMessage -import io.github.wulkanowy.data.db.entities.Attendance -import io.github.wulkanowy.data.db.entities.AttendanceSummary -import io.github.wulkanowy.data.db.entities.CompletedLesson -import io.github.wulkanowy.data.db.entities.Conference -import io.github.wulkanowy.data.db.entities.Exam -import io.github.wulkanowy.data.db.entities.Grade -import io.github.wulkanowy.data.db.entities.GradePartialStatistics -import io.github.wulkanowy.data.db.entities.GradePointsStatistics -import io.github.wulkanowy.data.db.entities.GradeSemesterStatistics -import io.github.wulkanowy.data.db.entities.GradeSummary -import io.github.wulkanowy.data.db.entities.Homework -import io.github.wulkanowy.data.db.entities.LuckyNumber -import io.github.wulkanowy.data.db.entities.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 -import io.github.wulkanowy.data.db.entities.Subject -import io.github.wulkanowy.data.db.entities.Teacher -import io.github.wulkanowy.data.db.entities.Timetable -import io.github.wulkanowy.data.db.entities.TimetableAdditional -import io.github.wulkanowy.data.db.entities.TimetableHeader -import io.github.wulkanowy.data.db.migrations.Migration10 -import io.github.wulkanowy.data.db.migrations.Migration11 -import io.github.wulkanowy.data.db.migrations.Migration12 -import io.github.wulkanowy.data.db.migrations.Migration13 -import io.github.wulkanowy.data.db.migrations.Migration14 -import io.github.wulkanowy.data.db.migrations.Migration15 -import io.github.wulkanowy.data.db.migrations.Migration16 -import io.github.wulkanowy.data.db.migrations.Migration17 -import io.github.wulkanowy.data.db.migrations.Migration18 -import io.github.wulkanowy.data.db.migrations.Migration19 -import io.github.wulkanowy.data.db.migrations.Migration2 -import io.github.wulkanowy.data.db.migrations.Migration20 -import io.github.wulkanowy.data.db.migrations.Migration21 -import io.github.wulkanowy.data.db.migrations.Migration22 -import io.github.wulkanowy.data.db.migrations.Migration23 -import io.github.wulkanowy.data.db.migrations.Migration24 -import io.github.wulkanowy.data.db.migrations.Migration25 -import io.github.wulkanowy.data.db.migrations.Migration26 -import io.github.wulkanowy.data.db.migrations.Migration27 -import io.github.wulkanowy.data.db.migrations.Migration28 -import io.github.wulkanowy.data.db.migrations.Migration29 -import io.github.wulkanowy.data.db.migrations.Migration3 -import io.github.wulkanowy.data.db.migrations.Migration30 -import io.github.wulkanowy.data.db.migrations.Migration31 -import io.github.wulkanowy.data.db.migrations.Migration32 -import io.github.wulkanowy.data.db.migrations.Migration33 -import io.github.wulkanowy.data.db.migrations.Migration34 -import io.github.wulkanowy.data.db.migrations.Migration35 -import io.github.wulkanowy.data.db.migrations.Migration36 -import io.github.wulkanowy.data.db.migrations.Migration37 -import io.github.wulkanowy.data.db.migrations.Migration38 -import io.github.wulkanowy.data.db.migrations.Migration39 -import io.github.wulkanowy.data.db.migrations.Migration4 -import io.github.wulkanowy.data.db.migrations.Migration40 -import io.github.wulkanowy.data.db.migrations.Migration41 -import io.github.wulkanowy.data.db.migrations.Migration42 -import io.github.wulkanowy.data.db.migrations.Migration43 -import io.github.wulkanowy.data.db.migrations.Migration44 -import io.github.wulkanowy.data.db.migrations.Migration5 -import io.github.wulkanowy.data.db.migrations.Migration6 -import io.github.wulkanowy.data.db.migrations.Migration7 -import io.github.wulkanowy.data.db.migrations.Migration8 -import io.github.wulkanowy.data.db.migrations.Migration9 +import io.github.wulkanowy.data.db.dao.* +import io.github.wulkanowy.data.db.entities.* +import io.github.wulkanowy.data.db.migrations.* import io.github.wulkanowy.utils.AppInfo import javax.inject.Singleton @@ -133,7 +30,7 @@ import javax.inject.Singleton Subject::class, LuckyNumber::class, CompletedLesson::class, - ReportingUnit::class, + Mailbox::class, Recipient::class, MobileDevice::class, Teacher::class, @@ -146,6 +43,11 @@ import javax.inject.Singleton Notification::class, AdminMessage::class ], + autoMigrations = [ + AutoMigration(from = 44, to = 45), + AutoMigration(from = 46, to = 47), + AutoMigration(from = 47, to = 48), + ], version = AppDatabase.VERSION_SCHEMA, exportSchema = true ) @@ -153,7 +55,7 @@ import javax.inject.Singleton abstract class AppDatabase : RoomDatabase() { companion object { - const val VERSION_SCHEMA = 44 + const val VERSION_SCHEMA = 51 fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf( Migration2(), @@ -198,7 +100,11 @@ abstract class AppDatabase : RoomDatabase() { Migration41(sharedPrefProvider), Migration42(), Migration43(), - Migration44() + Migration44(), + Migration46(), + Migration49(), + Migration50(), + Migration51(), ) fun newInstance( @@ -249,7 +155,7 @@ abstract class AppDatabase : RoomDatabase() { abstract val completedLessonsDao: CompletedLessonsDao - abstract val reportingUnitDao: ReportingUnitDao + abstract val mailboxDao: MailboxDao abstract val recipientDao: RecipientDao 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 1993c4338..9d3beae1f 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,40 +1,36 @@ package io.github.wulkanowy.data.db import androidx.room.TypeConverter +import io.github.wulkanowy.ui.modules.Destination +import io.github.wulkanowy.utils.toTimestamp import kotlinx.serialization.SerializationException import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json +import java.time.* +import java.util.* import java.time.Instant import java.time.LocalDate -import java.time.LocalDateTime import java.time.Month import java.time.ZoneOffset -import java.util.Date +import java.util.* class Converters { private val json = Json @TypeConverter - fun timestampToDate(value: Long?): LocalDate? = value?.run { - Date(value).toInstant().atZone(ZoneOffset.UTC).toLocalDate() - } + fun timestampToLocalDate(value: Long?): LocalDate? = + value?.let(::Date)?.toInstant()?.atZone(ZoneOffset.UTC)?.toLocalDate() @TypeConverter - fun dateToTimestamp(date: LocalDate?): Long? { - return date?.atStartOfDay()?.toInstant(ZoneOffset.UTC)?.toEpochMilli() - } + fun dateToTimestamp(date: LocalDate?): Long? = date?.toTimestamp() @TypeConverter - fun timestampToTime(value: Long?): LocalDateTime? = value?.let { - LocalDateTime.ofInstant(Instant.ofEpochMilli(value), ZoneOffset.UTC) - } + fun instantToTimestamp(instant: Instant?): Long? = instant?.toEpochMilli() @TypeConverter - fun timeToTimestamp(date: LocalDateTime?): Long? { - return date?.atZone(ZoneOffset.UTC)?.toInstant()?.toEpochMilli() - } + fun timestampToInstant(timestamp: Long?): Instant? = timestamp?.let(Instant::ofEpochMilli) @TypeConverter fun monthToInt(month: Month?) = month?.value @@ -65,4 +61,11 @@ class Converters { emptyList() // handle errors from old gson Pair serialized data } } + + @TypeConverter + fun destinationToString(destination: Destination) = json.encodeToString(destination) + + @TypeConverter + fun stringToDestination(destination: String): Destination = json.decodeFromString(destination) + } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/dao/BaseDao.kt b/app/src/main/java/io/github/wulkanowy/data/db/dao/BaseDao.kt index 048e9e3cd..056a5cbd1 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/dao/BaseDao.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/dao/BaseDao.kt @@ -2,11 +2,12 @@ package io.github.wulkanowy.data.db.dao import androidx.room.Delete import androidx.room.Insert +import androidx.room.OnConflictStrategy import androidx.room.Update interface BaseDao { - @Insert + @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertAll(items: List): List @Update diff --git a/app/src/main/java/io/github/wulkanowy/data/db/dao/ConferenceDao.kt b/app/src/main/java/io/github/wulkanowy/data/db/dao/ConferenceDao.kt index e84bad592..ca9da9ea9 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/dao/ConferenceDao.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/dao/ConferenceDao.kt @@ -4,7 +4,7 @@ import androidx.room.Dao import androidx.room.Query import io.github.wulkanowy.data.db.entities.Conference import kotlinx.coroutines.flow.Flow -import java.time.LocalDateTime +import java.time.Instant import javax.inject.Singleton @Dao @@ -12,5 +12,5 @@ import javax.inject.Singleton interface ConferenceDao : BaseDao { @Query("SELECT * FROM Conferences WHERE diary_id = :diaryId AND student_id = :studentId AND date >= :startDate") - fun loadAll(diaryId: Int, studentId: Int, startDate: LocalDateTime): Flow> + fun loadAll(diaryId: Int, studentId: Int, startDate: Instant): Flow> } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/dao/MailboxDao.kt b/app/src/main/java/io/github/wulkanowy/data/db/dao/MailboxDao.kt new file mode 100644 index 000000000..c44ecd0c2 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/db/dao/MailboxDao.kt @@ -0,0 +1,14 @@ +package io.github.wulkanowy.data.db.dao + +import androidx.room.Dao +import androidx.room.Query +import io.github.wulkanowy.data.db.entities.Mailbox +import javax.inject.Singleton + +@Singleton +@Dao +interface MailboxDao : BaseDao { + + @Query("SELECT * FROM Mailboxes WHERE userLoginId = :userLoginId ") + suspend fun loadAll(userLoginId: Int): List +} diff --git a/app/src/main/java/io/github/wulkanowy/data/db/dao/MessagesDao.kt b/app/src/main/java/io/github/wulkanowy/data/db/dao/MessagesDao.kt index 729ba6a68..8c730c9bc 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/dao/MessagesDao.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/dao/MessagesDao.kt @@ -11,9 +11,9 @@ import kotlinx.coroutines.flow.Flow interface MessagesDao : BaseDao { @Transaction - @Query("SELECT * FROM Messages WHERE student_id = :studentId AND message_id = :messageId") - fun loadMessageWithAttachment(studentId: Int, messageId: Int): Flow + @Query("SELECT * FROM Messages WHERE message_global_key = :messageGlobalKey") + fun loadMessageWithAttachment(messageGlobalKey: String): Flow - @Query("SELECT * FROM Messages WHERE student_id = :studentId AND folder_id = :folder ORDER BY date DESC") - fun loadAll(studentId: Int, folder: Int): Flow> + @Query("SELECT * FROM Messages WHERE mailbox_key = :mailboxKey AND folder_id = :folder ORDER BY date DESC") + fun loadAll(mailboxKey: String, folder: Int): Flow> } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/dao/MobileDeviceDao.kt b/app/src/main/java/io/github/wulkanowy/data/db/dao/MobileDeviceDao.kt index 081e859a5..96382cc10 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/dao/MobileDeviceDao.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/dao/MobileDeviceDao.kt @@ -8,6 +8,6 @@ import kotlinx.coroutines.flow.Flow @Dao interface MobileDeviceDao : BaseDao { - @Query("SELECT * FROM MobileDevices WHERE student_id = :userLoginId ORDER BY date DESC") + @Query("SELECT * FROM MobileDevices WHERE user_login_id = :userLoginId ORDER BY date DESC") fun loadAll(userLoginId: Int): Flow> } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/dao/RecipientDao.kt b/app/src/main/java/io/github/wulkanowy/data/db/dao/RecipientDao.kt index c2787ac3b..1956261eb 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/dao/RecipientDao.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/dao/RecipientDao.kt @@ -2,6 +2,7 @@ package io.github.wulkanowy.data.db.dao import androidx.room.Dao import androidx.room.Query +import io.github.wulkanowy.data.db.entities.MailboxType import io.github.wulkanowy.data.db.entities.Recipient import javax.inject.Singleton @@ -9,6 +10,6 @@ import javax.inject.Singleton @Dao interface RecipientDao : BaseDao { - @Query("SELECT * FROM Recipients WHERE student_id = :studentId AND unit_id = :unitId AND role = :role") - suspend fun loadAll(studentId: Int, unitId: Int, role: Int): List + @Query("SELECT * FROM Recipients WHERE type = :type AND studentMailboxGlobalKey = :studentMailboxGlobalKey") + suspend fun loadAll(type: MailboxType, studentMailboxGlobalKey: String): List } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/dao/ReportingUnitDao.kt b/app/src/main/java/io/github/wulkanowy/data/db/dao/ReportingUnitDao.kt deleted file mode 100644 index ca697eda8..000000000 --- a/app/src/main/java/io/github/wulkanowy/data/db/dao/ReportingUnitDao.kt +++ /dev/null @@ -1,17 +0,0 @@ -package io.github.wulkanowy.data.db.dao - -import androidx.room.Dao -import androidx.room.Query -import io.github.wulkanowy.data.db.entities.ReportingUnit -import javax.inject.Singleton - -@Singleton -@Dao -interface ReportingUnitDao : BaseDao { - - @Query("SELECT * FROM ReportingUnits WHERE student_id = :studentId") - suspend fun load(studentId: Int): List - - @Query("SELECT * FROM ReportingUnits WHERE student_id = :studentId AND real_id = :unitId") - suspend fun loadOne(studentId: Int, unitId: Int): ReportingUnit? -} diff --git a/app/src/main/java/io/github/wulkanowy/data/db/dao/SchoolAnnouncementDao.kt b/app/src/main/java/io/github/wulkanowy/data/db/dao/SchoolAnnouncementDao.kt index 15655f4ae..c32e4aba3 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/dao/SchoolAnnouncementDao.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/dao/SchoolAnnouncementDao.kt @@ -10,6 +10,6 @@ import javax.inject.Singleton @Singleton interface SchoolAnnouncementDao : BaseDao { - @Query("SELECT * FROM SchoolAnnouncements WHERE student_id = :studentId ORDER BY date DESC") - fun loadAll(studentId: Int): Flow> + @Query("SELECT * FROM SchoolAnnouncements WHERE user_login_id = :userLoginId ORDER BY date DESC") + fun loadAll(userLoginId: Int): Flow> } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/dao/StudentDao.kt b/app/src/main/java/io/github/wulkanowy/data/db/dao/StudentDao.kt index 3dda8a44b..87b3e0b32 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/dao/StudentDao.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/dao/StudentDao.kt @@ -1,12 +1,7 @@ package io.github.wulkanowy.data.db.dao -import androidx.room.Dao -import androidx.room.Delete -import androidx.room.Insert +import androidx.room.* import androidx.room.OnConflictStrategy.ABORT -import androidx.room.Query -import androidx.room.Transaction -import androidx.room.Update import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.StudentNickAndAvatar import io.github.wulkanowy.data.db.entities.StudentWithSemesters @@ -38,6 +33,10 @@ abstract class StudentDao { @Query("SELECT * FROM Students") abstract suspend fun loadStudentsWithSemesters(): List + @Transaction + @Query("SELECT * FROM Students WHERE id = :id") + abstract suspend fun loadStudentWithSemestersById(id: Long): StudentWithSemesters? + @Query("UPDATE Students SET is_current = 1 WHERE id = :id") abstract suspend fun updateCurrent(id: Long) diff --git a/app/src/main/java/io/github/wulkanowy/data/db/dao/TimetableAdditionalDao.kt b/app/src/main/java/io/github/wulkanowy/data/db/dao/TimetableAdditionalDao.kt index 335e003e1..914ce340a 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/dao/TimetableAdditionalDao.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/dao/TimetableAdditionalDao.kt @@ -5,6 +5,7 @@ import androidx.room.Query import io.github.wulkanowy.data.db.entities.TimetableAdditional import kotlinx.coroutines.flow.Flow import java.time.LocalDate +import java.util.UUID import javax.inject.Singleton @Dao @@ -12,5 +13,13 @@ import javax.inject.Singleton interface TimetableAdditionalDao : BaseDao { @Query("SELECT * FROM TimetableAdditional 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> + fun loadAll( + diaryId: Int, + studentId: Int, + from: LocalDate, + end: LocalDate + ): Flow> + + @Query("DELETE FROM TimetableAdditional WHERE repeat_id = :repeatId") + suspend fun deleteAllByRepeatId(repeatId: UUID) } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/Conference.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/Conference.kt index 4ad946508..ba3958dbc 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/entities/Conference.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/Conference.kt @@ -4,7 +4,7 @@ import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey import java.io.Serializable -import java.time.LocalDateTime +import java.time.Instant @Entity(tableName = "Conferences") data class Conference( @@ -27,7 +27,7 @@ data class Conference( @ColumnInfo(name = "conference_id") val conferenceId: Int, - val date: LocalDateTime + val date: Instant, ) : Serializable { @PrimaryKey(autoGenerate = true) diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/GradeSemesterStatistics.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/GradeSemesterStatistics.kt index e747271ce..9e08b86bc 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/entities/GradeSemesterStatistics.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/GradeSemesterStatistics.kt @@ -24,5 +24,8 @@ data class GradeSemesterStatistics( var id: Long = 0 @Transient - var average: String = "" + var classAverage: String = "" + + @Transient + var studentAverage: String = "" } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/GradeSummary.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/GradeSummary.kt index fb7b60bbc..a42832ced 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/entities/GradeSummary.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/GradeSummary.kt @@ -3,7 +3,7 @@ package io.github.wulkanowy.data.db.entities import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey -import java.time.LocalDateTime +import java.time.Instant @Entity(tableName = "GradesSummary") data class GradeSummary( @@ -45,8 +45,8 @@ data class GradeSummary( var isFinalGradeNotified: Boolean = true @ColumnInfo(name = "predicted_grade_last_change") - var predictedGradeLastChange: LocalDateTime = LocalDateTime.now() + var predictedGradeLastChange: Instant = Instant.now() @ColumnInfo(name = "final_grade_last_change") - var finalGradeLastChange: LocalDateTime = LocalDateTime.now() + var finalGradeLastChange: Instant = Instant.now() } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/Mailbox.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/Mailbox.kt new file mode 100644 index 000000000..7c08e481d --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/Mailbox.kt @@ -0,0 +1,25 @@ +package io.github.wulkanowy.data.db.entities + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "Mailboxes") +data class Mailbox( + + @PrimaryKey + val globalKey: String, + val fullName: String, + val userName: String, + val userLoginId: Int, + val studentName: String, + val schoolNameShort: String, + val type: MailboxType, +) + +enum class MailboxType { + STUDENT, + PARENT, + GUARDIAN, + EMPLOYEE, + UNKNOWN, +} diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/Message.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/Message.kt index 7b6e0dbf2..77874e03d 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/entities/Message.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/Message.kt @@ -4,40 +4,31 @@ import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey import java.io.Serializable -import java.time.LocalDateTime +import java.time.Instant @Entity(tableName = "Messages") data class Message( - @ColumnInfo(name = "student_id") - val studentId: Long, + @ColumnInfo(name = "message_global_key") + val messageGlobalKey: String, - @ColumnInfo(name = "real_id") - val realId: Int, + @ColumnInfo(name = "mailbox_key") + val mailboxKey: String, @ColumnInfo(name = "message_id") val messageId: Int, - @ColumnInfo(name = "sender_name") - val sender: String, - - @ColumnInfo(name = "sender_id") - val senderId: Int, - - @ColumnInfo(name = "recipient_name") - val recipient: String, + val correspondents: String, val subject: String, - val date: LocalDateTime, + val date: Instant, @ColumnInfo(name = "folder_id") val folderId: Int, var unread: Boolean, - val removed: Boolean, - @ColumnInfo(name = "has_attachments") val hasAttachments: Boolean ) : Serializable { @@ -48,11 +39,7 @@ data class Message( @ColumnInfo(name = "is_notified") var isNotified: Boolean = true - @ColumnInfo(name = "unread_by") - var unreadBy: Int = 0 - - @ColumnInfo(name = "read_by") - var readBy: Int = 0 - var content: String = "" + var sender: String? = null + var recipients: String? = null } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/MessageAttachment.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/MessageAttachment.kt index d1886e910..93f042999 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/entities/MessageAttachment.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/MessageAttachment.kt @@ -12,11 +12,8 @@ data class MessageAttachment( @ColumnInfo(name = "real_id") val realId: Int, - @ColumnInfo(name = "message_id") - val messageId: Int, - - @ColumnInfo(name = "one_drive_id") - val oneDriveId: String, + @ColumnInfo(name = "message_global_key") + val messageGlobalKey: String, @ColumnInfo(name = "url") val url: String, diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/MessageWithAttachment.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/MessageWithAttachment.kt index 2e7af0f40..cd468215d 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/entities/MessageWithAttachment.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/MessageWithAttachment.kt @@ -7,6 +7,6 @@ data class MessageWithAttachment( @Embedded val message: Message, - @Relation(parentColumn = "message_id", entityColumn = "message_id") + @Relation(parentColumn = "message_global_key", entityColumn = "message_global_key") val attachments: List ) diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/MobileDevice.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/MobileDevice.kt index 83d82c0b9..89b04ccc8 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/entities/MobileDevice.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/MobileDevice.kt @@ -4,12 +4,12 @@ import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey import java.io.Serializable -import java.time.LocalDateTime +import java.time.Instant @Entity(tableName = "MobileDevices") data class MobileDevice( - @ColumnInfo(name = "student_id") + @ColumnInfo(name = "user_login_id") val userLoginId: Int, @ColumnInfo(name = "device_id") @@ -17,7 +17,7 @@ data class MobileDevice( val name: String, - val date: LocalDateTime + val date: Instant, ) : Serializable { @PrimaryKey(autoGenerate = true) 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 index 740137f0f..c3267f24e 100644 --- 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 @@ -4,7 +4,8 @@ import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey import io.github.wulkanowy.services.sync.notifications.NotificationType -import java.time.LocalDateTime +import io.github.wulkanowy.ui.modules.Destination +import java.time.Instant @Entity(tableName = "Notifications") data class Notification( @@ -18,7 +19,10 @@ data class Notification( val type: NotificationType, - val date: LocalDateTime, + @ColumnInfo(defaultValue = "{\"type\":\"io.github.wulkanowy.ui.modules.Destination.Dashboard\"}") + val destination: Destination, + + val date: Instant, val data: String? = null ) { 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 223322705..d09742cd2 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 @@ -1,6 +1,5 @@ package io.github.wulkanowy.data.db.entities -import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey import java.io.Serializable @@ -8,32 +7,16 @@ import java.io.Serializable @kotlinx.serialization.Serializable @Entity(tableName = "Recipients") data class Recipient( - - @ColumnInfo(name = "student_id") - val studentId: Int, - - @ColumnInfo(name = "real_id") - val realId: String, - - val name: String, - - @ColumnInfo(name = "real_name") - val realName: String, - - @ColumnInfo(name = "login_id") - val loginId: Int, - - @ColumnInfo(name = "unit_id") - val unitId: Int, - - val role: Int, - - val hash: String - + val mailboxGlobalKey: String, + val studentMailboxGlobalKey: String, + val fullName: String, + val userName: String, + val schoolShortName: String, + val type: MailboxType, ) : Serializable { @PrimaryKey(autoGenerate = true) var id: Long = 0 - override fun toString() = name + override fun toString() = userName } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/ReportingUnit.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/ReportingUnit.kt deleted file mode 100644 index 0570a2ffd..000000000 --- a/app/src/main/java/io/github/wulkanowy/data/db/entities/ReportingUnit.kt +++ /dev/null @@ -1,32 +0,0 @@ -package io.github.wulkanowy.data.db.entities - -import androidx.room.ColumnInfo -import androidx.room.Entity -import androidx.room.PrimaryKey -import java.io.Serializable - -@Entity(tableName = "ReportingUnits") -data class ReportingUnit( - - @ColumnInfo(name = "student_id") - val studentId: Int, - - @ColumnInfo(name = "real_id") - val unitId: Int, - - @ColumnInfo(name = "short") - val shortName: String, - - @ColumnInfo(name = "sender_id") - val senderId: Int, - - @ColumnInfo(name = "sender_name") - val senderName: String, - - val roles: List - -) : Serializable { - - @PrimaryKey(autoGenerate = true) - var id: Long = 0 -} diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/SchoolAnnouncement.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/SchoolAnnouncement.kt index c8731bded..25e27ef18 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/entities/SchoolAnnouncement.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/SchoolAnnouncement.kt @@ -9,8 +9,8 @@ import java.time.LocalDate @Entity(tableName = "SchoolAnnouncements") data class SchoolAnnouncement( - @ColumnInfo(name = "student_id") - val studentId: Int, + @ColumnInfo(name = "user_login_id") + val userLoginId: Int, val date: LocalDate, diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/Semester.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/Semester.kt index 3b1f0add5..187890c9b 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/entities/Semester.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/Semester.kt @@ -7,7 +7,12 @@ import androidx.room.PrimaryKey import java.io.Serializable import java.time.LocalDate -@Entity(tableName = "Semesters", indices = [Index(value = ["student_id", "diary_id", "semester_id"], unique = true)]) +@Entity( + tableName = "Semesters", indices = [Index( + value = ["student_id", "diary_id", "kindergarten_diary_id", "semester_id"], + unique = true + )] +) data class Semester( @ColumnInfo(name = "student_id") @@ -16,6 +21,9 @@ data class Semester( @ColumnInfo(name = "diary_id") val diaryId: Int, + @ColumnInfo(name = "kindergarten_diary_id", defaultValue = "0") + val kindergartenDiaryId: Int, + @ColumnInfo(name = "diary_name") val diaryName: String, @@ -37,12 +45,11 @@ data class Semester( @ColumnInfo(name = "unit_id") val unitId: Int -): Serializable { +) : Serializable { @PrimaryKey(autoGenerate = true) var id: Long = 0 - @ColumnInfo(name = "is_current") var current: Boolean = false } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/Student.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/Student.kt index af9fe831a..76da9643d 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/entities/Student.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/Student.kt @@ -5,7 +5,7 @@ import androidx.room.Entity import androidx.room.Index import androidx.room.PrimaryKey import java.io.Serializable -import java.time.LocalDateTime +import java.time.Instant @Entity( tableName = "Students", @@ -74,7 +74,7 @@ data class Student( val isCurrent: Boolean, @ColumnInfo(name = "registration_date") - val registrationDate: LocalDateTime + val registrationDate: Instant, ) : Serializable { @PrimaryKey(autoGenerate = true) 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 29b3737bc..d23d388f9 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 @@ -4,8 +4,8 @@ import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey import java.io.Serializable +import java.time.Instant import java.time.LocalDate -import java.time.LocalDateTime @Entity(tableName = "Timetable") data class Timetable( @@ -18,9 +18,9 @@ data class Timetable( val number: Int, - val start: LocalDateTime, + val start: Instant, - val end: LocalDateTime, + val end: Instant, val date: LocalDate, diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/TimetableAdditional.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/TimetableAdditional.kt index c1f1365f9..478026102 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/entities/TimetableAdditional.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/TimetableAdditional.kt @@ -4,8 +4,9 @@ import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey import java.io.Serializable +import java.time.Instant import java.time.LocalDate -import java.time.LocalDateTime +import java.util.* @Entity(tableName = "TimetableAdditional") data class TimetableAdditional( @@ -16,9 +17,9 @@ data class TimetableAdditional( @ColumnInfo(name = "diary_id") val diaryId: Int, - val start: LocalDateTime, + val start: Instant, - val end: LocalDateTime, + val end: Instant, val date: LocalDate, @@ -27,4 +28,10 @@ data class TimetableAdditional( @PrimaryKey(autoGenerate = true) var id: Long = 0 + + @ColumnInfo(name = "repeat_id", defaultValue = "NULL") + var repeatId: UUID? = null + + @ColumnInfo(name = "is_added_by_user", defaultValue = "0") + var isAddedByUser: Boolean = false } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration12.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration12.kt index 1dc38e14c..c827b82ba 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration12.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration12.kt @@ -43,12 +43,14 @@ class Migration12 : Migration(11, 12) { private fun getStudentsIds(database: SupportSQLiteDatabase): List { val students = mutableListOf() - val studentsCursor = database.query("SELECT student_id FROM Students") - if (studentsCursor.moveToFirst()) { - do { - students.add(studentsCursor.getInt(0)) - } while (studentsCursor.moveToNext()) + database.query("SELECT student_id FROM Students").use { + if (it.moveToFirst()) { + do { + students.add(it.getInt(0)) + } while (it.moveToNext()) + } } + return students } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration13.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration13.kt index 0cf8cd9b0..36de1e837 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration13.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration13.kt @@ -25,12 +25,14 @@ class Migration13 : Migration(12, 13) { private fun getStudentsIds(database: SupportSQLiteDatabase): MutableList> { val students = mutableListOf>() - val studentsCursor = database.query("SELECT id, school_name FROM Students") - if (studentsCursor.moveToFirst()) { - do { - students.add(studentsCursor.getInt(0) to studentsCursor.getString(1)) - } while (studentsCursor.moveToNext()) + database.query("SELECT id, school_name FROM Students").use { + if (it.moveToFirst()) { + do { + students.add(it.getInt(0) to it.getString(1)) + } while (it.moveToNext()) + } } + return students } @@ -42,12 +44,14 @@ class Migration13 : Migration(12, 13) { private fun getStudentsAndClassIds(database: SupportSQLiteDatabase): List> { val students = mutableListOf>() - val studentsCursor = database.query("SELECT student_id, class_id FROM Students") - if (studentsCursor.moveToFirst()) { - do { - students.add(studentsCursor.getInt(0) to studentsCursor.getInt(1)) - } while (studentsCursor.moveToNext()) + database.query("SELECT student_id, class_id FROM Students").use { + if (it.moveToFirst()) { + do { + students.add(it.getInt(0) to it.getInt(1)) + } while (it.moveToNext()) + } } + return students } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration27.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration27.kt index 6592228a1..5c60beead 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration27.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration27.kt @@ -22,24 +22,28 @@ class Migration27 : Migration(26, 27) { private fun getStudentsIdsAndNames(database: SupportSQLiteDatabase): MutableList> { val students = mutableListOf>() - val studentsCursor = database.query("SELECT id, user_login_id, student_name FROM Students") - if (studentsCursor.moveToFirst()) { - do { - students.add(Triple(studentsCursor.getLong(0), studentsCursor.getInt(1), studentsCursor.getString(2))) - } while (studentsCursor.moveToNext()) + database.query("SELECT id, user_login_id, student_name FROM Students").use { + if (it.moveToFirst()) { + do { + students.add(Triple(it.getLong(0), it.getInt(1), it.getString(2))) + } while (it.moveToNext()) + } } + return students } private fun getReportingUnits(database: SupportSQLiteDatabase): MutableList> { val units = mutableListOf>() - val unitsCursor = database.query("SELECT sender_id, sender_name FROM ReportingUnits") - if (unitsCursor.moveToFirst()) { - do { - units.add(unitsCursor.getInt(0) to unitsCursor.getString(1)) - } while (unitsCursor.moveToNext()) + database.query("SELECT sender_id, sender_name FROM ReportingUnits").use { + if (it.moveToFirst()) { + do { + units.add(it.getInt(0) to it.getString(1)) + } while (it.moveToNext()) + } } + return units } } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration35.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration35.kt index cc540388c..f63431d00 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration35.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration35.kt @@ -10,15 +10,17 @@ class Migration35(private val appInfo: AppInfo) : Migration(34, 35) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL("ALTER TABLE Students ADD COLUMN `avatar_color` INTEGER NOT NULL DEFAULT 0") - val studentsCursor = database.query("SELECT * FROM Students") - - while (studentsCursor.moveToNext()) { - val studentId = studentsCursor.getLongOrNull(0) - database.execSQL( - """UPDATE Students - SET avatar_color = ${appInfo.defaultColorsForAvatar.random()} - WHERE id = $studentId""" - ) + database.query("SELECT * FROM Students").use { + while (it.moveToNext()) { + val studentId = it.getLongOrNull(0) + database.execSQL( + """ + UPDATE Students + SET avatar_color = ${appInfo.defaultColorsForAvatar.random()} + WHERE id = $studentId + """ + ) + } } } } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration46.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration46.kt new file mode 100644 index 000000000..d3fa5cf93 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration46.kt @@ -0,0 +1,102 @@ +package io.github.wulkanowy.data.db.migrations + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase +import java.time.Instant +import java.time.ZoneId +import java.time.ZoneOffset + +class Migration46 : Migration(45, 46) { + + override fun migrate(database: SupportSQLiteDatabase) { + migrateConferences(database) + migrateMessages(database) + migrateMobileDevices(database) + migrateNotifications(database) + migrateTimetable(database) + migrateTimetableAdditional(database) + } + + private fun migrateConferences(database: SupportSQLiteDatabase) { + database.query("SELECT * FROM Conferences").use { + while (it.moveToNext()) { + val id = it.getLong(it.getColumnIndexOrThrow("id")) + val timestampLocal = it.getLong(it.getColumnIndexOrThrow("date")) + val timestampUtc = timestampLocal.timestampLocalToUTC() + + database.execSQL("UPDATE Conferences SET date = $timestampUtc WHERE id = $id") + } + } + } + + private fun migrateMessages(database: SupportSQLiteDatabase) { + database.query("SELECT * FROM Messages").use { + while (it.moveToNext()) { + val id = it.getLong(it.getColumnIndexOrThrow("id")) + val timestampLocal = it.getLong(it.getColumnIndexOrThrow("date")) + val timestampUtc = timestampLocal.timestampLocalToUTC() + + database.execSQL("UPDATE Messages SET date = $timestampUtc WHERE id = $id") + } + } + } + + private fun migrateMobileDevices(database: SupportSQLiteDatabase) { + database.query("SELECT * FROM MobileDevices").use { + while (it.moveToNext()) { + val id = it.getLong(it.getColumnIndexOrThrow("id")) + val timestampLocal = it.getLong(it.getColumnIndexOrThrow("date")) + val timestampUtc = timestampLocal.timestampLocalToUTC() + + database.execSQL("UPDATE MobileDevices SET date = $timestampUtc WHERE id = $id") + } + } + } + + private fun migrateNotifications(database: SupportSQLiteDatabase) { + database.query("SELECT * FROM Notifications").use { + while (it.moveToNext()) { + val id = it.getLong(it.getColumnIndexOrThrow("id")) + val timestampLocal = it.getLong(it.getColumnIndexOrThrow("date")) + val timestampUtc = timestampLocal.timestampLocalToUTC() + + database.execSQL("UPDATE Notifications SET date = $timestampUtc WHERE id = $id") + } + } + } + + private fun migrateTimetable(database: SupportSQLiteDatabase) { + database.query("SELECT * FROM Timetable").use { + while (it.moveToNext()) { + val id = it.getLong(it.getColumnIndexOrThrow("id")) + val timestampLocalStart = it.getLong(it.getColumnIndexOrThrow("start")) + val timestampLocalEnd = it.getLong(it.getColumnIndexOrThrow("end")) + val timestampUtcStart = timestampLocalStart.timestampLocalToUTC() + val timestampUtcEnd = timestampLocalEnd.timestampLocalToUTC() + + database.execSQL("UPDATE Timetable SET start = $timestampUtcStart, end = $timestampUtcEnd WHERE id = $id") + } + } + } + + private fun migrateTimetableAdditional(database: SupportSQLiteDatabase) { + database.query("SELECT * FROM TimetableAdditional").use { + while (it.moveToNext()) { + val id = it.getLong(it.getColumnIndexOrThrow("id")) + val timestampLocalStart = it.getLong(it.getColumnIndexOrThrow("start")) + val timestampLocalEnd = it.getLong(it.getColumnIndexOrThrow("end")) + val timestampUtcStart = timestampLocalStart.timestampLocalToUTC() + val timestampUtcEnd = timestampLocalEnd.timestampLocalToUTC() + + database.execSQL("UPDATE TimetableAdditional SET start = $timestampUtcStart, end = $timestampUtcEnd WHERE id = $id") + } + } + } + + private fun Long.timestampLocalToUTC(): Long = Instant.ofEpochMilli(this) + .atZone(ZoneOffset.UTC) + .withZoneSameLocal(ZoneId.of("Europe/Warsaw")) + .withZoneSameInstant(ZoneId.systemDefault()) + .toInstant() + .toEpochMilli() +} diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration49.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration49.kt new file mode 100644 index 000000000..6e1de19d4 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration49.kt @@ -0,0 +1,23 @@ +package io.github.wulkanowy.data.db.migrations + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +class Migration49 : Migration(48, 49) { + + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("DROP TABLE IF EXISTS SchoolAnnouncements") + + database.execSQL( + """ + CREATE TABLE IF NOT EXISTS `SchoolAnnouncements` ( + `user_login_id` INTEGER NOT NULL, + `date` INTEGER NOT NULL, + `subject` TEXT NOT NULL, + `content` TEXT NOT NULL, + `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + `is_notified` INTEGER NOT NULL) + """.trimIndent() + ) + } +} diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration50.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration50.kt new file mode 100644 index 000000000..d45a81570 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration50.kt @@ -0,0 +1,21 @@ +package io.github.wulkanowy.data.db.migrations + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +class Migration50 : Migration(49, 50) { + + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("DROP TABLE IF EXISTS MobileDevices") + database.execSQL( + """ + CREATE TABLE IF NOT EXISTS `MobileDevices` ( + `user_login_id` INTEGER NOT NULL, + `device_id` INTEGER NOT NULL, + `name` TEXT NOT NULL, + `date` INTEGER NOT NULL, + `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL) + """.trimIndent() + ) + } +} diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration51.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration51.kt new file mode 100644 index 000000000..e78e2e3a7 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration51.kt @@ -0,0 +1,88 @@ +package io.github.wulkanowy.data.db.migrations + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +class Migration51 : Migration(50, 51) { + + override fun migrate(database: SupportSQLiteDatabase) { + createMailboxTable(database) + recreateMessagesTable(database) + recreateMessageAttachmentsTable(database) + recreateRecipientsTable(database) + deleteReportingUnitTable(database) + } + + private fun createMailboxTable(database: SupportSQLiteDatabase) { + database.execSQL("DROP TABLE IF EXISTS Mailboxes") + database.execSQL( + """ + CREATE TABLE IF NOT EXISTS `Mailboxes` ( + `globalKey` TEXT NOT NULL, + `fullName` TEXT NOT NULL, + `userName` TEXT NOT NULL, + `userLoginId` INTEGER NOT NULL, + `studentName` TEXT NOT NULL, + `schoolNameShort` TEXT NOT NULL, + `type` TEXT NOT NULL, + PRIMARY KEY(`globalKey`) + )""".trimIndent() + ) + } + + private fun recreateMessagesTable(database: SupportSQLiteDatabase) { + database.execSQL("DROP TABLE IF EXISTS Messages") + database.execSQL( + """ + CREATE TABLE IF NOT EXISTS `Messages` ( + `message_global_key` TEXT NOT NULL, + `mailbox_key` TEXT NOT NULL, + `message_id` INTEGER NOT NULL, + `correspondents` TEXT NOT NULL, + `subject` TEXT NOT NULL, + `date` INTEGER NOT NULL, + `folder_id` INTEGER NOT NULL, + `unread` INTEGER NOT NULL, + `has_attachments` INTEGER NOT NULL, + `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + `is_notified` INTEGER NOT NULL, + `content` TEXT NOT NULL, + `sender` TEXT, `recipients` TEXT + )""".trimIndent() + ) + } + + private fun recreateMessageAttachmentsTable(database: SupportSQLiteDatabase) { + database.execSQL("DROP TABLE IF EXISTS MessageAttachments") + database.execSQL( + """ + CREATE TABLE IF NOT EXISTS `MessageAttachments` ( + `real_id` INTEGER NOT NULL, + `message_global_key` TEXT NOT NULL, + `url` TEXT NOT NULL, + `filename` TEXT NOT NULL, + PRIMARY KEY(`real_id`) + )""".trimIndent() + ) + } + + private fun recreateRecipientsTable(database: SupportSQLiteDatabase) { + database.execSQL("DROP TABLE IF EXISTS Recipients") + database.execSQL( + """ + CREATE TABLE IF NOT EXISTS `Recipients` ( + `mailboxGlobalKey` TEXT NOT NULL, + `studentMailboxGlobalKey` TEXT NOT NULL, + `fullName` TEXT NOT NULL, + `userName` TEXT NOT NULL, + `schoolShortName` TEXT NOT NULL, + `type` TEXT NOT NULL, + `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL + )""".trimIndent() + ) + } + + private fun deleteReportingUnitTable(database: SupportSQLiteDatabase) { + database.execSQL("DROP TABLE IF EXISTS ReportingUnits") + } +} diff --git a/app/src/main/java/io/github/wulkanowy/data/enums/GradeSortingMode.kt b/app/src/main/java/io/github/wulkanowy/data/enums/GradeSortingMode.kt index c5c0196c9..a7aa4cc2f 100644 --- a/app/src/main/java/io/github/wulkanowy/data/enums/GradeSortingMode.kt +++ b/app/src/main/java/io/github/wulkanowy/data/enums/GradeSortingMode.kt @@ -2,9 +2,10 @@ package io.github.wulkanowy.data.enums enum class GradeSortingMode(val value: String) { ALPHABETIC("alphabetic"), - DATE("date"); + DATE("date"), + AVERAGE("average"); companion object { fun getByValue(value: String) = values().find { it.value == value } ?: ALPHABETIC } -} \ No newline at end of file +} diff --git a/app/src/main/java/io/github/wulkanowy/data/mappers/ConferenceMapper.kt b/app/src/main/java/io/github/wulkanowy/data/mappers/ConferenceMapper.kt index 52dc9b30b..17a9e5cdb 100644 --- a/app/src/main/java/io/github/wulkanowy/data/mappers/ConferenceMapper.kt +++ b/app/src/main/java/io/github/wulkanowy/data/mappers/ConferenceMapper.kt @@ -10,7 +10,7 @@ fun List.mapToEntities(semester: Semester) = map { diaryId = semester.diaryId, agenda = it.agenda, conferenceId = it.id, - date = it.date, + date = it.dateZoned.toInstant(), presentOnConference = it.presentOnConference, subject = it.subject, title = it.title diff --git a/app/src/main/java/io/github/wulkanowy/data/mappers/DirectorInformationMapper.kt b/app/src/main/java/io/github/wulkanowy/data/mappers/DirectorInformationMapper.kt index e6bf000b1..16f1bbac0 100644 --- a/app/src/main/java/io/github/wulkanowy/data/mappers/DirectorInformationMapper.kt +++ b/app/src/main/java/io/github/wulkanowy/data/mappers/DirectorInformationMapper.kt @@ -6,7 +6,7 @@ import io.github.wulkanowy.sdk.pojo.DirectorInformation as SdkDirectorInformatio fun List.mapToEntities(student: Student) = map { SchoolAnnouncement( - studentId = student.studentId, + userLoginId = student.userLoginId, date = it.date, subject = it.subject, content = it.content, diff --git a/app/src/main/java/io/github/wulkanowy/data/mappers/MailboxMapper.kt b/app/src/main/java/io/github/wulkanowy/data/mappers/MailboxMapper.kt new file mode 100644 index 000000000..2ccca1b90 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/mappers/MailboxMapper.kt @@ -0,0 +1,18 @@ +package io.github.wulkanowy.data.mappers + +import io.github.wulkanowy.data.db.entities.Mailbox +import io.github.wulkanowy.data.db.entities.MailboxType +import io.github.wulkanowy.data.db.entities.Student +import io.github.wulkanowy.sdk.pojo.Mailbox as SdkMailbox + +fun List.mapToEntities(student: Student) = map { + Mailbox( + globalKey = it.globalKey, + fullName = it.fullName, + userName = it.userName, + userLoginId = student.userLoginId, + studentName = it.studentName, + schoolNameShort = it.schoolNameShort, + type = MailboxType.valueOf(it.type.name), + ) +} diff --git a/app/src/main/java/io/github/wulkanowy/data/mappers/MessageMapper.kt b/app/src/main/java/io/github/wulkanowy/data/mappers/MessageMapper.kt index 913e4d030..2e7967f0e 100644 --- a/app/src/main/java/io/github/wulkanowy/data/mappers/MessageMapper.kt +++ b/app/src/main/java/io/github/wulkanowy/data/mappers/MessageMapper.kt @@ -1,40 +1,31 @@ package io.github.wulkanowy.data.mappers -import io.github.wulkanowy.data.db.entities.Message -import io.github.wulkanowy.data.db.entities.MessageAttachment -import io.github.wulkanowy.data.db.entities.Recipient -import io.github.wulkanowy.data.db.entities.Student -import java.time.LocalDateTime +import io.github.wulkanowy.data.db.entities.* +import io.github.wulkanowy.sdk.pojo.MailboxType import io.github.wulkanowy.sdk.pojo.Message as SdkMessage import io.github.wulkanowy.sdk.pojo.MessageAttachment as SdkMessageAttachment import io.github.wulkanowy.sdk.pojo.Recipient as SdkRecipient -fun List.mapToEntities(student: Student) = map { +fun List.mapToEntities(mailbox: Mailbox) = map { Message( - studentId = student.id, - realId = it.id ?: 0, - messageId = it.messageId ?: 0, - sender = it.sender?.name.orEmpty(), - senderId = it.sender?.loginId ?: 0, - recipient = it.recipients.singleOrNull()?.name ?: "Wielu adresatów", + messageGlobalKey = it.globalKey, + mailboxKey = mailbox.globalKey, + messageId = it.id, + correspondents = it.correspondents, subject = it.subject.trim(), - date = it.date ?: LocalDateTime.now(), + date = it.dateZoned.toInstant(), folderId = it.folderId, - unread = it.unread ?: false, - removed = it.removed, + unread = it.unread, hasAttachments = it.hasAttachments ).apply { content = it.content.orEmpty() - unreadBy = it.unreadBy ?: 0 - readBy = it.readBy ?: 0 } } -fun List.mapToEntities() = map { +fun List.mapToEntities(messageGlobalKey: String) = map { MessageAttachment( - realId = it.id, - messageId = it.messageId, - oneDriveId = it.oneDriveId, + messageGlobalKey = messageGlobalKey, + realId = it.url.hashCode(), url = it.url, filename = it.filename ) @@ -42,12 +33,11 @@ fun List.mapToEntities() = map { fun List.mapFromEntities() = map { SdkRecipient( - id = it.realId, - name = it.realName, - loginId = it.loginId, - reportingUnitId = it.unitId, - role = it.role, - hash = it.hash, - shortName = it.name + fullName = it.fullName, + userName = it.userName, + studentName = it.userName, + mailboxGlobalKey = it.mailboxGlobalKey, + schoolNameShort = it.schoolShortName, + type = MailboxType.valueOf(it.type.name), ) } diff --git a/app/src/main/java/io/github/wulkanowy/data/mappers/MobileDeviceMapper.kt b/app/src/main/java/io/github/wulkanowy/data/mappers/MobileDeviceMapper.kt index f0c375bfa..1a1c501f6 100644 --- a/app/src/main/java/io/github/wulkanowy/data/mappers/MobileDeviceMapper.kt +++ b/app/src/main/java/io/github/wulkanowy/data/mappers/MobileDeviceMapper.kt @@ -1,15 +1,15 @@ package io.github.wulkanowy.data.mappers import io.github.wulkanowy.data.db.entities.MobileDevice -import io.github.wulkanowy.data.db.entities.Semester +import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.pojos.MobileDeviceToken -import io.github.wulkanowy.sdk.pojo.Token as SdkToken import io.github.wulkanowy.sdk.pojo.Device as SdkDevice +import io.github.wulkanowy.sdk.pojo.Token as SdkToken -fun List.mapToEntities(semester: Semester) = map { +fun List.mapToEntities(student: Student) = map { MobileDevice( - userLoginId = semester.studentId, - date = it.createDate, + userLoginId = student.userLoginId, + date = it.createDateZoned.toInstant(), deviceId = it.id, name = it.name ) diff --git a/app/src/main/java/io/github/wulkanowy/data/mappers/RecipientMapper.kt b/app/src/main/java/io/github/wulkanowy/data/mappers/RecipientMapper.kt index 80bddaab1..eb993a0f0 100644 --- a/app/src/main/java/io/github/wulkanowy/data/mappers/RecipientMapper.kt +++ b/app/src/main/java/io/github/wulkanowy/data/mappers/RecipientMapper.kt @@ -1,17 +1,16 @@ package io.github.wulkanowy.data.mappers +import io.github.wulkanowy.data.db.entities.MailboxType import io.github.wulkanowy.data.db.entities.Recipient import io.github.wulkanowy.sdk.pojo.Recipient as SdkRecipient -fun List.mapToEntities(userLoginId: Int) = map { +fun List.mapToEntities(studentMailboxGlobalKey: String) = map { Recipient( - studentId = userLoginId, - realId = it.id, - realName = it.name, - name = it.shortName, - hash = it.hash, - loginId = it.loginId, - role = it.role, - unitId = it.reportingUnitId ?: 0 + mailboxGlobalKey = it.mailboxGlobalKey, + fullName = it.fullName, + userName = it.userName, + studentMailboxGlobalKey = studentMailboxGlobalKey, + schoolShortName = it.schoolNameShort, + type = MailboxType.valueOf(it.type.name), ) } diff --git a/app/src/main/java/io/github/wulkanowy/data/mappers/ReportingUnitMapper.kt b/app/src/main/java/io/github/wulkanowy/data/mappers/ReportingUnitMapper.kt deleted file mode 100644 index 6a21d59fc..000000000 --- a/app/src/main/java/io/github/wulkanowy/data/mappers/ReportingUnitMapper.kt +++ /dev/null @@ -1,16 +0,0 @@ -package io.github.wulkanowy.data.mappers - -import io.github.wulkanowy.data.db.entities.ReportingUnit -import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.sdk.pojo.ReportingUnit as SdkReportingUnit - -fun List.mapToEntities(student: Student) = map { - ReportingUnit( - studentId = student.id.toInt(), - unitId = it.id, - roles = it.roles, - senderId = it.senderId, - senderName = it.senderName, - shortName = it.short - ) -} diff --git a/app/src/main/java/io/github/wulkanowy/data/mappers/SemesterMapper.kt b/app/src/main/java/io/github/wulkanowy/data/mappers/SemesterMapper.kt index acd93a91a..67d68a1e3 100644 --- a/app/src/main/java/io/github/wulkanowy/data/mappers/SemesterMapper.kt +++ b/app/src/main/java/io/github/wulkanowy/data/mappers/SemesterMapper.kt @@ -7,6 +7,7 @@ fun List.mapToEntities(studentId: Int) = map { Semester( studentId = studentId, diaryId = it.diaryId, + kindergartenDiaryId = it.kindergartenDiaryId, diaryName = it.diaryName, schoolYear = it.schoolYear, semesterId = it.semesterId, diff --git a/app/src/main/java/io/github/wulkanowy/data/mappers/StudentMapper.kt b/app/src/main/java/io/github/wulkanowy/data/mappers/StudentMapper.kt index c93323038..a2110d7f5 100644 --- a/app/src/main/java/io/github/wulkanowy/data/mappers/StudentMapper.kt +++ b/app/src/main/java/io/github/wulkanowy/data/mappers/StudentMapper.kt @@ -2,7 +2,7 @@ package io.github.wulkanowy.data.mappers import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.StudentWithSemesters -import java.time.LocalDateTime +import java.time.Instant import io.github.wulkanowy.sdk.pojo.Student as SdkStudent fun List.mapToEntities(password: String = "", colors: List) = map { @@ -24,7 +24,7 @@ fun List.mapToEntities(password: String = "", colors: List) = scrapperBaseUrl = it.scrapperBaseUrl, loginType = it.loginType.name, isCurrent = false, - registrationDate = LocalDateTime.now(), + registrationDate = Instant.now(), mobileBaseUrl = it.mobileBaseUrl, privateKey = it.privateKey, certificateKey = it.certificateKey, diff --git a/app/src/main/java/io/github/wulkanowy/data/mappers/TimetableMapper.kt b/app/src/main/java/io/github/wulkanowy/data/mappers/TimetableMapper.kt index 045101c42..e55aa3cf7 100644 --- a/app/src/main/java/io/github/wulkanowy/data/mappers/TimetableMapper.kt +++ b/app/src/main/java/io/github/wulkanowy/data/mappers/TimetableMapper.kt @@ -21,8 +21,8 @@ fun List.mapToEntities(semester: Semester) = map { studentId = semester.studentId, diaryId = semester.diaryId, number = it.number, - start = it.start, - end = it.end, + start = it.startZoned.toInstant(), + end = it.endZoned.toInstant(), date = it.date, subject = it.subject, subjectOld = it.subjectOld, @@ -45,8 +45,8 @@ fun List.mapToEntities(semester: Semester) = map { diaryId = semester.diaryId, subject = it.subject, date = it.date, - start = it.start, - end = it.end + start = it.startZoned.toInstant(), + end = it.endZoned.toInstant(), ) } 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 index 0748ba647..f4fd0fc8a 100644 --- a/app/src/main/java/io/github/wulkanowy/data/pojos/NotificationData.kt +++ b/app/src/main/java/io/github/wulkanowy/data/pojos/NotificationData.kt @@ -1,10 +1,10 @@ package io.github.wulkanowy.data.pojos -import android.content.Intent import io.github.wulkanowy.services.sync.notifications.NotificationType +import io.github.wulkanowy.ui.modules.Destination data class NotificationData( - val intentToStart: Intent, + val destination: Destination, val title: String, val content: String ) @@ -13,7 +13,7 @@ data class GroupNotificationData( val notificationDataList: List, val title: String, val content: String, - val intentToStart: Intent, + val destination: Destination, 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 index 1b17e3bf3..c9655b722 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/AdminMessageRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/AdminMessageRepository.kt @@ -3,9 +3,8 @@ 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.data.networkBoundResource 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 @@ -14,23 +13,18 @@ import javax.inject.Singleton class AdminMessageRepository @Inject constructor( private val adminMessageService: AdminMessageService, private val adminMessageDao: AdminMessageDao, - private val appInfo: AppInfo, - private val refreshHelper: AutoRefreshHelper, + private val appInfo: AppInfo ) { private val saveFetchResultMutex = Mutex() - private val cacheKey = "admin_messages" - - suspend fun getAdminMessages(student: Student, forceRefresh: Boolean) = networkBoundResource( + suspend fun getAdminMessages(student: Student) = networkBoundResource( mutex = saveFetchResultMutex, + isResultEmpty = { it == null }, query = { adminMessageDao.loadAll() }, fetch = { adminMessageService.getAdminMessages() }, - shouldFetch = { - refreshHelper.shouldBeRefreshed(cacheKey) || forceRefresh - }, + shouldFetch = { true }, saveFetchResult = { oldItems, newItems -> adminMessageDao.removeOldAndSaveNew(oldItems, newItems) - refreshHelper.updateLastRefreshTimestamp(cacheKey) }, showSavedOnLoading = false, mapResult = { adminMessages -> 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 ec9198175..9aa6562a6 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 @@ -5,15 +5,10 @@ import io.github.wulkanowy.data.db.entities.Attendance import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.mappers.mapToEntities +import io.github.wulkanowy.data.networkBoundResource import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.pojo.Absent -import io.github.wulkanowy.utils.AutoRefreshHelper -import io.github.wulkanowy.utils.getRefreshKey -import io.github.wulkanowy.utils.init -import io.github.wulkanowy.utils.monday -import io.github.wulkanowy.utils.networkBoundResource -import io.github.wulkanowy.utils.sunday -import io.github.wulkanowy.utils.uniqueSubtract +import io.github.wulkanowy.utils.* import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.sync.Mutex import java.time.LocalDate @@ -42,6 +37,7 @@ class AttendanceRepository @Inject constructor( notify: Boolean = false, ) = networkBoundResource( mutex = saveFetchResultMutex, + isResultEmpty = { it.isEmpty() }, shouldFetch = { val isExpired = refreshHelper.shouldBeRefreshed( key = getRefreshKey(cacheKey, semester, start, end) @@ -52,7 +48,8 @@ class AttendanceRepository @Inject constructor( attendanceDb.loadAll(semester.diaryId, semester.studentId, start.monday, end.sunday) }, fetch = { - sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) + sdk.init(student) + .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) .getAttendance(start.monday, end.sunday, semester.semesterId) .mapToEntities(semester) }, @@ -90,7 +87,8 @@ class AttendanceRepository @Inject constructor( timeId = attendance.timeId ) } - sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) + sdk.init(student) + .switchDiary(semester.diaryId, semester.kindergartenDiaryId, 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 bc1fb2343..8e0709135 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 @@ -4,11 +4,11 @@ import io.github.wulkanowy.data.db.dao.AttendanceSummaryDao import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.mappers.mapToEntities +import io.github.wulkanowy.data.networkBoundResource import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.AutoRefreshHelper import io.github.wulkanowy.utils.getRefreshKey import io.github.wulkanowy.utils.init -import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.uniqueSubtract import kotlinx.coroutines.sync.Mutex import javax.inject.Inject @@ -32,13 +32,15 @@ class AttendanceSummaryRepository @Inject constructor( forceRefresh: Boolean, ) = networkBoundResource( mutex = saveFetchResultMutex, + isResultEmpty = { it.isEmpty() }, shouldFetch = { val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, semester)) it.isEmpty() || forceRefresh || isExpired }, query = { attendanceDb.loadAll(semester.diaryId, semester.studentId, subjectId) }, fetch = { - sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) + sdk.init(student) + .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) .getAttendanceSummary(subjectId) .mapToEntities(semester, subjectId) }, diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/CompletedLessonsRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/CompletedLessonsRepository.kt index c2e5a7217..8f393cadb 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 @@ -4,14 +4,9 @@ import io.github.wulkanowy.data.db.dao.CompletedLessonsDao import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.mappers.mapToEntities +import io.github.wulkanowy.data.networkBoundResource import io.github.wulkanowy.sdk.Sdk -import io.github.wulkanowy.utils.AutoRefreshHelper -import io.github.wulkanowy.utils.getRefreshKey -import io.github.wulkanowy.utils.init -import io.github.wulkanowy.utils.monday -import io.github.wulkanowy.utils.networkBoundResource -import io.github.wulkanowy.utils.sunday -import io.github.wulkanowy.utils.uniqueSubtract +import io.github.wulkanowy.utils.* import kotlinx.coroutines.sync.Mutex import java.time.LocalDate import javax.inject.Inject @@ -36,6 +31,7 @@ class CompletedLessonsRepository @Inject constructor( forceRefresh: Boolean, ) = networkBoundResource( mutex = saveFetchResultMutex, + isResultEmpty = { it.isEmpty() }, shouldFetch = { val isExpired = refreshHelper.shouldBeRefreshed( key = getRefreshKey(cacheKey, semester, start, end) @@ -51,7 +47,8 @@ class CompletedLessonsRepository @Inject constructor( ) }, fetch = { - sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) + sdk.init(student) + .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) .getCompletedLessons(start.monday, end.sunday) .mapToEntities(semester) }, diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/ConferenceRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/ConferenceRepository.kt index e32271833..83204cab0 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 @@ -5,17 +5,15 @@ import io.github.wulkanowy.data.db.entities.Conference import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.mappers.mapToEntities +import io.github.wulkanowy.data.networkBoundResource import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.AutoRefreshHelper import io.github.wulkanowy.utils.getRefreshKey import io.github.wulkanowy.utils.init -import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.uniqueSubtract import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.sync.Mutex import java.time.Instant -import java.time.LocalDateTime -import java.time.ZoneOffset import javax.inject.Inject import javax.inject.Singleton @@ -35,9 +33,10 @@ class ConferenceRepository @Inject constructor( semester: Semester, forceRefresh: Boolean, notify: Boolean = false, - startDate: LocalDateTime = LocalDateTime.ofInstant(Instant.EPOCH, ZoneOffset.UTC), + startDate: Instant = Instant.EPOCH, ) = networkBoundResource( mutex = saveFetchResultMutex, + isResultEmpty = { it.isEmpty() }, shouldFetch = { val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, semester)) it.isEmpty() || forceRefresh || isExpired @@ -46,7 +45,8 @@ class ConferenceRepository @Inject constructor( conferenceDb.loadAll(semester.diaryId, student.studentId, startDate) }, fetch = { - sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) + sdk.init(student) + .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) .getConferences() .mapToEntities(semester) .filter { it.date >= startDate } @@ -66,7 +66,7 @@ class ConferenceRepository @Inject constructor( conferenceDb.loadAll( diaryId = semester.diaryId, studentId = semester.studentId, - startDate = LocalDateTime.ofInstant(Instant.EPOCH, ZoneOffset.UTC) + startDate = Instant.EPOCH, ) suspend fun updateConference(conference: List) = conferenceDb.updateAll(conference) 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 9bdac0658..faa80b93e 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 @@ -5,14 +5,9 @@ import io.github.wulkanowy.data.db.entities.Exam import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.mappers.mapToEntities +import io.github.wulkanowy.data.networkBoundResource import io.github.wulkanowy.sdk.Sdk -import io.github.wulkanowy.utils.AutoRefreshHelper -import io.github.wulkanowy.utils.endExamsDay -import io.github.wulkanowy.utils.getRefreshKey -import io.github.wulkanowy.utils.init -import io.github.wulkanowy.utils.networkBoundResource -import io.github.wulkanowy.utils.startExamsDay -import io.github.wulkanowy.utils.uniqueSubtract +import io.github.wulkanowy.utils.* import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.sync.Mutex import java.time.LocalDate @@ -39,6 +34,7 @@ class ExamRepository @Inject constructor( notify: Boolean = false, ) = networkBoundResource( mutex = saveFetchResultMutex, + isResultEmpty = { it.isEmpty() }, shouldFetch = { val isExpired = refreshHelper.shouldBeRefreshed( key = getRefreshKey(cacheKey, semester, start, end) @@ -54,7 +50,8 @@ class ExamRepository @Inject constructor( ) }, fetch = { - sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) + sdk.init(student) + .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) .getExams(start.startExamsDay, start.endExamsDay, semester.semesterId) .mapToEntities(semester) }, 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 6c574b48a..f5f895d82 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 @@ -7,17 +7,14 @@ import io.github.wulkanowy.data.db.entities.GradeSummary import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.mappers.mapToEntities +import io.github.wulkanowy.data.networkBoundResource import io.github.wulkanowy.sdk.Sdk -import io.github.wulkanowy.utils.AutoRefreshHelper -import io.github.wulkanowy.utils.getRefreshKey -import io.github.wulkanowy.utils.init -import io.github.wulkanowy.utils.networkBoundResource -import io.github.wulkanowy.utils.uniqueSubtract +import io.github.wulkanowy.utils.* import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map import kotlinx.coroutines.sync.Mutex -import java.time.LocalDateTime +import java.time.Instant import javax.inject.Inject import javax.inject.Singleton @@ -40,6 +37,10 @@ class GradeRepository @Inject constructor( notify: Boolean = false, ) = networkBoundResource( mutex = saveFetchResultMutex, + isResultEmpty = { + //When details is empty and summary is not, app will not use summary cache - edge case + it.first.isEmpty() + }, shouldFetch = { (details, summaries) -> val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, semester)) details.isEmpty() || summaries.isEmpty() || forceRefresh || isExpired @@ -51,7 +52,7 @@ class GradeRepository @Inject constructor( }, fetch = { val (details, summary) = sdk.init(student) - .switchDiary(semester.diaryId, semester.schoolYear) + .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) .getGrades(semester.semesterId) details.mapToEntities(semester) to summary.mapToEntities(semester) @@ -70,8 +71,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 { @@ -101,13 +102,13 @@ class GradeRepository @Inject constructor( } summary.predictedGradeLastChange = when { - oldSummary == null -> LocalDateTime.now() - summary.predictedGrade != oldSummary.predictedGrade -> LocalDateTime.now() + oldSummary == null -> Instant.now() + summary.predictedGrade != oldSummary.predictedGrade -> Instant.now() else -> oldSummary.predictedGradeLastChange } summary.finalGradeLastChange = when { - oldSummary == null -> LocalDateTime.now() - summary.finalGrade != oldSummary.finalGrade -> LocalDateTime.now() + oldSummary == null -> Instant.now() + summary.finalGrade != oldSummary.finalGrade -> Instant.now() else -> oldSummary.finalGradeLastChange } }) 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 6c36f163b..9fa06c497 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 @@ -11,14 +11,14 @@ import io.github.wulkanowy.data.mappers.mapPartialToStatisticItems import io.github.wulkanowy.data.mappers.mapPointsToStatisticsItems import io.github.wulkanowy.data.mappers.mapSemesterToStatisticItems import io.github.wulkanowy.data.mappers.mapToEntities +import io.github.wulkanowy.data.networkBoundResource import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.AutoRefreshHelper import io.github.wulkanowy.utils.getRefreshKey import io.github.wulkanowy.utils.init -import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.uniqueSubtract import kotlinx.coroutines.sync.Mutex -import java.util.Locale +import java.util.* import javax.inject.Inject import javax.inject.Singleton @@ -46,6 +46,7 @@ class GradeStatisticsRepository @Inject constructor( forceRefresh: Boolean, ) = networkBoundResource( mutex = partialMutex, + isResultEmpty = { it.isEmpty() }, shouldFetch = { val isExpired = refreshHelper.shouldBeRefreshed( key = getRefreshKey(partialCacheKey, semester) @@ -54,7 +55,8 @@ class GradeStatisticsRepository @Inject constructor( }, query = { gradePartialStatisticsDb.loadAll(semester.semesterId, semester.studentId) }, fetch = { - sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) + sdk.init(student) + .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) .getGradesPartialStatistics(semester.semesterId) .mapToEntities(semester) }, @@ -66,20 +68,16 @@ class GradeStatisticsRepository @Inject constructor( mapResult = { items -> when (subjectName) { "Wszystkie" -> { - val numerator = items.map { - it.classAverage.replace(",", ".").toDoubleOrNull() ?: .0 - }.filterNot { it == .0 } - (items.reversed() + GradePartialStatistics( + val summaryItem = GradePartialStatistics( studentId = semester.studentId, semesterId = semester.semesterId, subject = subjectName, - classAverage = if (numerator.isEmpty()) "" else numerator.average().let { - "%.2f".format(Locale.FRANCE, it) - }, - studentAverage = "", + classAverage = items.map { it.classAverage }.getSummaryAverage(), + studentAverage = items.map { it.studentAverage }.getSummaryAverage(), classAmounts = items.map { it.classAmounts }.sumGradeAmounts(), studentAmounts = items.map { it.studentAmounts }.sumGradeAmounts() - )).reversed() + ) + listOf(summaryItem) + items } else -> items.filter { it.subject == subjectName } }.mapPartialToStatisticItems() @@ -93,6 +91,7 @@ class GradeStatisticsRepository @Inject constructor( forceRefresh: Boolean, ) = networkBoundResource( mutex = semesterMutex, + isResultEmpty = { it.isEmpty() }, shouldFetch = { val isExpired = refreshHelper.shouldBeRefreshed( key = getRefreshKey(semesterCacheKey, semester) @@ -101,7 +100,8 @@ class GradeStatisticsRepository @Inject constructor( }, query = { gradeSemesterStatisticsDb.loadAll(semester.semesterId, semester.studentId) }, fetch = { - sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) + sdk.init(student) + .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) .getGradesSemesterStatistics(semester.semesterId) .mapToEntities(semester) }, @@ -114,29 +114,29 @@ class GradeStatisticsRepository @Inject constructor( val itemsWithAverage = items.map { item -> item.copy().apply { val denominator = item.amounts.sum() - average = if (denominator == 0) "" else { + classAverage = if (denominator == 0) "" else { (item.amounts.mapIndexed { gradeValue, amount -> (gradeValue + 1) * amount - }.sum().toDouble() / denominator).let { - "%.2f".format(Locale.FRANCE, it) - } + }.sum().toDouble() / denominator).asAverageString() } } } when (subjectName) { - "Wszystkie" -> (itemsWithAverage.reversed() + GradeSemesterStatistics( - studentId = semester.studentId, - semesterId = semester.semesterId, - subject = subjectName, - amounts = itemsWithAverage.map { it.amounts }.sumGradeAmounts(), - studentGrade = 0 - ).apply { - average = itemsWithAverage.mapNotNull { - it.average.replace(",", ".").toDoubleOrNull() - }.average().let { - "%.2f".format(Locale.FRANCE, it) + "Wszystkie" -> { + val summaryItem = GradeSemesterStatistics( + studentId = semester.studentId, + semesterId = semester.semesterId, + subject = subjectName, + amounts = itemsWithAverage.map { it.amounts }.sumGradeAmounts(), + studentGrade = 0, + ).apply { + classAverage = itemsWithAverage.map { it.classAverage }.getSummaryAverage() + studentAverage = items + .mapNotNull { summary -> summary.studentGrade.takeIf { it != 0 } } + .average().asAverageString() } - }).reversed() + listOf(summaryItem) + itemsWithAverage + } else -> itemsWithAverage.filter { it.subject == subjectName } }.mapSemesterToStatisticItems() } @@ -149,13 +149,15 @@ class GradeStatisticsRepository @Inject constructor( forceRefresh: Boolean, ) = networkBoundResource( mutex = pointsMutex, + isResultEmpty = { it.isEmpty() }, 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) + sdk.init(student) + .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) .getGradesPointsStatistics(semester.semesterId) .mapToEntities(semester) }, @@ -172,6 +174,19 @@ class GradeStatisticsRepository @Inject constructor( } ) + private fun List.getSummaryAverage(): String { + val averages = mapNotNull { + it.replace(",", ".").toDoubleOrNull() + } + + return averages.average() + .asAverageString() + .takeIf { averages.isNotEmpty() } + .orEmpty() + } + + private fun Double.asAverageString(): String = "%.2f".format(Locale.FRANCE, this) + private fun List>.sumGradeAmounts(): List { val result = mutableListOf(0, 0, 0, 0, 0, 0) forEach { 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 95a375a45..f564824de 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 @@ -5,14 +5,9 @@ import io.github.wulkanowy.data.db.entities.Homework import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.mappers.mapToEntities +import io.github.wulkanowy.data.networkBoundResource import io.github.wulkanowy.sdk.Sdk -import io.github.wulkanowy.utils.AutoRefreshHelper -import io.github.wulkanowy.utils.getRefreshKey -import io.github.wulkanowy.utils.init -import io.github.wulkanowy.utils.monday -import io.github.wulkanowy.utils.networkBoundResource -import io.github.wulkanowy.utils.sunday -import io.github.wulkanowy.utils.uniqueSubtract +import io.github.wulkanowy.utils.* import kotlinx.coroutines.sync.Mutex import java.time.LocalDate import javax.inject.Inject @@ -38,6 +33,7 @@ class HomeworkRepository @Inject constructor( notify: Boolean = false, ) = networkBoundResource( mutex = saveFetchResultMutex, + isResultEmpty = { it.isEmpty() }, shouldFetch = { val isExpired = refreshHelper.shouldBeRefreshed( key = getRefreshKey(cacheKey, semester, start, end) @@ -53,7 +49,8 @@ class HomeworkRepository @Inject constructor( ) }, fetch = { - sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) + sdk.init(student) + .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) .getHomework(start.monday, end.sunday) .mapToEntities(semester) }, diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/LuckyNumberRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/LuckyNumberRepository.kt index 41e824e57..87e8410f1 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 @@ -4,9 +4,9 @@ import io.github.wulkanowy.data.db.dao.LuckyNumberDao import io.github.wulkanowy.data.db.entities.LuckyNumber import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.mappers.mapToEntity +import io.github.wulkanowy.data.networkBoundResource import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.init -import io.github.wulkanowy.utils.networkBoundResource import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.sync.Mutex @@ -29,6 +29,7 @@ class LuckyNumberRepository @Inject constructor( notify: Boolean = false, ) = networkBoundResource( mutex = saveFetchResultMutex, + isResultEmpty = { it == null }, shouldFetch = { it == null || forceRefresh }, query = { luckyNumberDb.load(student.studentId, now()) }, fetch = { diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/MailboxRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/MailboxRepository.kt new file mode 100644 index 000000000..c571937ad --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/MailboxRepository.kt @@ -0,0 +1,85 @@ +package io.github.wulkanowy.data.repositories + +import io.github.wulkanowy.data.db.dao.MailboxDao +import io.github.wulkanowy.data.db.entities.Mailbox +import io.github.wulkanowy.data.db.entities.Student +import io.github.wulkanowy.data.mappers.mapToEntities +import io.github.wulkanowy.sdk.Sdk +import io.github.wulkanowy.utils.AutoRefreshHelper +import io.github.wulkanowy.utils.getRefreshKey +import io.github.wulkanowy.utils.init +import io.github.wulkanowy.utils.uniqueSubtract +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class MailboxRepository @Inject constructor( + private val mailboxDao: MailboxDao, + private val sdk: Sdk, + private val refreshHelper: AutoRefreshHelper, +) { + private val cacheKey = "mailboxes" + + suspend fun refreshMailboxes(student: Student) { + val new = sdk.init(student).getMailboxes().mapToEntities(student) + val old = mailboxDao.loadAll(student.userLoginId) + + mailboxDao.deleteAll(old uniqueSubtract new) + mailboxDao.insertAll(new uniqueSubtract old) + + refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, student)) + } + + suspend fun getMailbox(student: Student): Mailbox { + val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, student)) + val mailboxes = mailboxDao.loadAll(student.userLoginId) + val mailbox = mailboxes.filterByStudent(student) + + return if (isExpired || mailbox == null) { + refreshMailboxes(student) + val newMailbox = mailboxDao.loadAll(student.userLoginId).filterByStudent(student) + + requireNotNull(newMailbox) { + "Mailbox for ${student.userName} - ${student.studentName} not found! Saved mailboxes: $mailboxes" + } + + newMailbox + } else mailbox + } + + private fun List.filterByStudent(student: Student): Mailbox? { + val normalizedStudentName = student.studentName.normalizeStudentName() + + return find { + it.studentName.normalizeStudentName() == normalizedStudentName + } ?: singleOrNull { + it.studentName.getFirstAndLastPart() == normalizedStudentName.getFirstAndLastPart() + } ?: singleOrNull { + it.studentName.getUnauthorizedVersion() == normalizedStudentName + } + } + + private fun String.normalizeStudentName(): String { + return trim().split(" ") + .filter { it.isNotBlank() } + .joinToString(" ") { part -> + part.lowercase().replaceFirstChar { it.uppercase() } + } + } + + private fun String.getFirstAndLastPart(): String { + val parts = normalizeStudentName().split(" ") + + val endParts = parts.filterIndexed { i, _ -> + i == 0 || parts.size - 1 == i + } + return endParts.joinToString(" ") + } + + private fun String.getUnauthorizedVersion(): String { + return normalizeStudentName().split(" ") + .joinToString(" ") { + it.first() + "*".repeat(it.length - 1) + } + } +} 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 224c69bd7..e7428762e 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 @@ -7,31 +7,27 @@ import io.github.wulkanowy.data.Resource import io.github.wulkanowy.data.db.SharedPrefProvider import io.github.wulkanowy.data.db.dao.MessageAttachmentDao import io.github.wulkanowy.data.db.dao.MessagesDao -import io.github.wulkanowy.data.db.entities.Message -import io.github.wulkanowy.data.db.entities.MessageWithAttachment -import io.github.wulkanowy.data.db.entities.Recipient -import io.github.wulkanowy.data.db.entities.Semester -import io.github.wulkanowy.data.db.entities.Student +import io.github.wulkanowy.data.db.entities.* import io.github.wulkanowy.data.enums.MessageFolder import io.github.wulkanowy.data.enums.MessageFolder.RECEIVED +import io.github.wulkanowy.data.enums.MessageFolder.TRASHED import io.github.wulkanowy.data.mappers.mapFromEntities import io.github.wulkanowy.data.mappers.mapToEntities +import io.github.wulkanowy.data.networkBoundResource import io.github.wulkanowy.data.pojos.MessageDraft import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.pojo.Folder -import io.github.wulkanowy.sdk.pojo.SentMessage 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 import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first 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 import javax.inject.Singleton @@ -53,89 +49,72 @@ class MessageRepository @Inject constructor( @Suppress("UNUSED_PARAMETER") fun getMessages( student: Student, - semester: Semester, + mailbox: Mailbox, folder: MessageFolder, forceRefresh: Boolean, notify: Boolean = false, ): Flow>> = networkBoundResource( mutex = saveFetchResultMutex, + isResultEmpty = { it.isEmpty() }, shouldFetch = { val isExpired = refreshHelper.shouldBeRefreshed( key = getRefreshKey(cacheKey, student, folder) ) it.isEmpty() || forceRefresh || isExpired }, - query = { messagesDb.loadAll(student.id.toInt(), folder.id) }, + query = { messagesDb.loadAll(mailbox.globalKey, folder.id) }, fetch = { - sdk.init(student).getMessages(Folder.valueOf(folder.name), now().minusMonths(3), now()) - .mapToEntities(student) + sdk.init(student).getMessages( + folder = Folder.valueOf(folder.name), + mailboxKey = mailbox.globalKey, + ).mapToEntities(mailbox) }, saveFetchResult = { old, new -> messagesDb.deleteAll(old uniqueSubtract new) messagesDb.insertAll((new uniqueSubtract old).onEach { it.isNotified = !notify }) - messagesDb.updateAll(getMessagesWithReadByChange(old, new, !notify)) refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, student, folder)) } ) - private fun getMessagesWithReadByChange( - old: List, - new: List, - setNotified: Boolean - ): List { - val oldMeta = old.map { Triple(it, it.readBy, it.unreadBy) } - val newMeta = new.map { Triple(it, it.readBy, it.unreadBy) } - - val updatedItems = newMeta uniqueSubtract oldMeta - - return updatedItems.map { - val oldItem = old.find { item -> item.messageId == it.first.messageId } - it.first.apply { - id = oldItem?.id ?: 0 - isNotified = oldItem?.isNotified ?: setNotified - content = oldItem?.content.orEmpty() - } - } - } - fun getMessage( student: Student, message: Message, markAsRead: Boolean = false, ): Flow> = networkBoundResource( + isResultEmpty = { it?.message?.content.isNullOrBlank() }, shouldFetch = { - checkNotNull(it, { "This message no longer exist!" }) - Timber.d("Message content in db empty: ${it.message.content.isEmpty()}") - it.message.unread || it.message.content.isEmpty() + checkNotNull(it) { "This message no longer exist!" } + Timber.d("Message content in db empty: ${it.message.content.isBlank()}") + it.message.unread || it.message.content.isBlank() }, - query = { messagesDb.loadMessageWithAttachment(student.id.toInt(), message.messageId) }, + query = { messagesDb.loadMessageWithAttachment(message.messageGlobalKey) }, fetch = { - sdk.init(student).getMessageDetails( - messageId = it!!.message.messageId, - folderId = message.folderId, - read = markAsRead, - id = message.realId - ).let { details -> - details.content to details.attachments.mapToEntities() - } + sdk.init(student).getMessageDetails(it!!.message.messageGlobalKey, markAsRead) }, - saveFetchResult = { old, (downloadedMessage, attachments) -> - checkNotNull(old, { "Fetched message no longer exist!" }) - messagesDb.updateAll(listOf(old.message.apply { - id = old.message.id - unread = !markAsRead - content = content.ifBlank { downloadedMessage } - })) - messageAttachmentDao.insertAttachments(attachments) + saveFetchResult = { old, new -> + checkNotNull(old) { "Fetched message no longer exist!" } + messagesDb.updateAll( + listOf(old.message.apply { + id = message.id + unread = !markAsRead + sender = new.sender + recipients = new.recipients.singleOrNull() ?: "Wielu adresatów" + content = content.ifBlank { new.content } + }) + ) + messageAttachmentDao.insertAttachments( + items = new.attachments.mapToEntities(message.messageGlobalKey), + ) + Timber.d("Message ${message.messageId} with blank content: ${old.message.content.isBlank()}, marked as read") } ) - fun getMessagesFromDatabase(student: Student): Flow> { - return messagesDb.loadAll(student.id.toInt(), RECEIVED.id) + fun getMessagesFromDatabase(mailbox: Mailbox): Flow> { + return messagesDb.loadAll(mailbox.globalKey, RECEIVED.id) } suspend fun updateMessages(messages: List) { @@ -147,31 +126,54 @@ class MessageRepository @Inject constructor( subject: String, content: String, recipients: List, - ): SentMessage = sdk.init(student).sendMessage( - subject = subject, - content = content, - recipients = recipients.mapFromEntities() - ) + mailboxId: String, + ) { + sdk.init(student).sendMessage( + subject = subject, + content = content, + recipients = recipients.mapFromEntities(), + mailboxId = mailboxId, + ) + } - suspend fun deleteMessage(student: Student, message: Message) { - val isDeleted = sdk.init(student).deleteMessages( - messages = listOf(message.messageId), message.folderId + suspend fun deleteMessages(student: Student, mailbox: Mailbox, messages: List) { + val firstMessage = messages.first() + sdk.init(student).deleteMessages( + messages = messages.map { it.messageGlobalKey }, + removeForever = firstMessage.folderId == TRASHED.id, ) - if (message.folderId != MessageFolder.TRASHED.id && isDeleted) { - val deletedMessage = message.copy(folderId = MessageFolder.TRASHED.id).apply { - id = message.id - content = message.content + if (firstMessage.folderId != TRASHED.id) { + val deletedMessages = messages.map { + it.copy(folderId = TRASHED.id) + .apply { + id = it.id + content = it.content + sender = it.sender + recipients = it.recipients + } } - messagesDb.updateAll(listOf(deletedMessage)) - } else messagesDb.deleteAll(listOf(message)) + + messagesDb.updateAll(deletedMessages) + } else messagesDb.deleteAll(messages) + + getMessages( + student = student, + mailbox = mailbox, + folder = TRASHED, + forceRefresh = true, + ).first() + } + + suspend fun deleteMessage(student: Student, mailbox: Mailbox, message: Message) { + deleteMessages(student, mailbox, listOf(message)) } var draftMessage: MessageDraft? - get() = sharedPrefProvider.getString(context.getString(R.string.pref_key_message_send_draft)) + get() = sharedPrefProvider.getString(context.getString(R.string.pref_key_message_draft)) ?.let { json.decodeFromString(it) } set(value) = sharedPrefProvider.putString( - context.getString(R.string.pref_key_message_send_draft), + context.getString(R.string.pref_key_message_draft), 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 bf17cbbc5..07c6959e3 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 @@ -6,12 +6,12 @@ import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.data.mappers.mapToMobileDeviceToken +import io.github.wulkanowy.data.networkBoundResource import io.github.wulkanowy.data.pojos.MobileDeviceToken import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.AutoRefreshHelper import io.github.wulkanowy.utils.getRefreshKey import io.github.wulkanowy.utils.init -import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.uniqueSubtract import kotlinx.coroutines.sync.Mutex import javax.inject.Inject @@ -34,15 +34,17 @@ class MobileDeviceRepository @Inject constructor( forceRefresh: Boolean, ) = networkBoundResource( mutex = saveFetchResultMutex, + isResultEmpty = { it.isEmpty() }, shouldFetch = { val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, student)) it.isEmpty() || forceRefresh || isExpired }, - query = { mobileDb.loadAll(student.userLoginId.takeIf { it != 0 } ?: student.studentId) }, + query = { mobileDb.loadAll(student.userLoginId) }, fetch = { - sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) + sdk.init(student) + .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) .getRegisteredDevices() - .mapToEntities(semester) + .mapToEntities(student) }, saveFetchResult = { old, new -> mobileDb.deleteAll(old uniqueSubtract new) @@ -53,14 +55,16 @@ class MobileDeviceRepository @Inject constructor( ) suspend fun unregisterDevice(student: Student, semester: Semester, device: MobileDevice) { - sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) + sdk.init(student) + .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) .unregisterDevice(device.deviceId) mobileDb.deleteAll(listOf(device)) } suspend fun getToken(student: Student, semester: Semester): MobileDeviceToken { - return sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) + return sdk.init(student) + .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) .getToken() .mapToMobileDeviceToken() } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/NoteRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/NoteRepository.kt index c1738b36e..e5d7bc5cb 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 @@ -5,12 +5,9 @@ import io.github.wulkanowy.data.db.entities.Note import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.mappers.mapToEntities +import io.github.wulkanowy.data.networkBoundResource import io.github.wulkanowy.sdk.Sdk -import io.github.wulkanowy.utils.AutoRefreshHelper -import io.github.wulkanowy.utils.getRefreshKey -import io.github.wulkanowy.utils.init -import io.github.wulkanowy.utils.networkBoundResource -import io.github.wulkanowy.utils.uniqueSubtract +import io.github.wulkanowy.utils.* import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.sync.Mutex import javax.inject.Inject @@ -34,6 +31,7 @@ class NoteRepository @Inject constructor( notify: Boolean = false, ) = networkBoundResource( mutex = saveFetchResultMutex, + isResultEmpty = { it.isEmpty() }, shouldFetch = { val isExpired = refreshHelper.shouldBeRefreshed( getRefreshKey(cacheKey, semester) @@ -42,7 +40,8 @@ class NoteRepository @Inject constructor( }, query = { noteDb.loadAll(student.studentId) }, fetch = { - sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) + sdk.init(student) + .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) .getNotes(semester.semesterId) .mapToEntities(semester) }, diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt index 48eac48a2..486538e0c 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 @@ -7,24 +7,16 @@ import com.fredporciuncula.flow.preferences.FlowSharedPreferences import com.fredporciuncula.flow.preferences.Preference import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R -import io.github.wulkanowy.data.enums.AppTheme -import io.github.wulkanowy.data.enums.GradeColorTheme -import io.github.wulkanowy.data.enums.GradeExpandMode -import io.github.wulkanowy.data.enums.GradeSortingMode -import io.github.wulkanowy.data.enums.TimetableMode -import io.github.wulkanowy.sdk.toLocalDate +import io.github.wulkanowy.data.enums.* import io.github.wulkanowy.ui.modules.dashboard.DashboardItem import io.github.wulkanowy.ui.modules.grade.GradeAverageMode -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.time.LocalDate -import java.time.LocalDateTime +import java.time.Instant import javax.inject.Inject import javax.inject.Singleton @@ -133,6 +125,12 @@ class PreferencesRepository @Inject constructor( R.bool.pref_default_notification_piggyback ) + val isNotificationPiggybackRemoveOriginalEnabled: Boolean + get() = getBoolean( + R.string.pref_key_notifications_piggyback_cancel_original, + R.bool.pref_default_notification_piggyback_cancel_original + ) + val isDebugNotificationEnableKey = context.getString(R.string.pref_key_notification_debug) val isDebugNotificationEnable: Boolean get() = getBoolean(isDebugNotificationEnableKey, R.bool.pref_default_notification_debug) @@ -202,10 +200,10 @@ class PreferencesRepository @Inject constructor( R.bool.pref_default_optional_arithmetic_average ) - var lasSyncDate: LocalDateTime + var lasSyncDate: Instant? 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() + .takeIf { it != 0L }?.let(Instant::ofEpochMilli) + set(value) = sharedPref.edit().putLong("last_sync_date", value?.toEpochMilli() ?: 0).apply() var dashboardItemsPosition: Map? get() { @@ -224,19 +222,31 @@ class PreferencesRepository @Inject constructor( get() = selectedDashboardTilesPreference.asFlow() .map { set -> set.map { DashboardItem.Tile.valueOf(it) } - .plus(DashboardItem.Tile.ACCOUNT) - .plus(DashboardItem.Tile.ADMIN_MESSAGE) + .plus( + listOfNotNull( + DashboardItem.Tile.ACCOUNT, + DashboardItem.Tile.ADMIN_MESSAGE, + DashboardItem.Tile.ADS.takeIf { isAdsEnabled } + ) + ) .toSet() } var selectedDashboardTiles: Set get() = selectedDashboardTilesPreference.get() .map { DashboardItem.Tile.valueOf(it) } - .plus(DashboardItem.Tile.ACCOUNT) - .plus(DashboardItem.Tile.ADMIN_MESSAGE) + .plus( + listOfNotNull( + DashboardItem.Tile.ACCOUNT, + DashboardItem.Tile.ADMIN_MESSAGE, + DashboardItem.Tile.ADS.takeIf { isAdsEnabled } + ) + ) .toSet() set(value) { - val filteredValue = value.filterNot { it == DashboardItem.Tile.ACCOUNT } + val filteredValue = value.filterNot { + it == DashboardItem.Tile.ACCOUNT || it == DashboardItem.Tile.ADMIN_MESSAGE + } .map { it.name } .toSet() @@ -264,15 +274,47 @@ class PreferencesRepository @Inject constructor( get() = sharedPref.getInt(PREF_KEY_IN_APP_REVIEW_COUNT, 0) set(value) = sharedPref.edit().putInt(PREF_KEY_IN_APP_REVIEW_COUNT, value).apply() - var inAppReviewDate: LocalDate? + var inAppReviewDate: Instant? 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() + ?.let(Instant::ofEpochMilli) + set(value) = sharedPref.edit { + putLong(PREF_KEY_IN_APP_REVIEW_DATE, value?.toEpochMilli() ?: 0) + } var isAppReviewDone: Boolean get() = sharedPref.getBoolean(PREF_KEY_IN_APP_REVIEW_DONE, false) - set(value) = sharedPref.edit().putBoolean(PREF_KEY_IN_APP_REVIEW_DONE, value).apply() + set(value) = sharedPref.edit { putBoolean(PREF_KEY_IN_APP_REVIEW_DONE, value) } + + var isAppSupportShown: Boolean + get() = sharedPref.getBoolean(PREF_KEY_APP_SUPPORT_SHOWN, false) + set(value) = sharedPref.edit { putBoolean(PREF_KEY_APP_SUPPORT_SHOWN, value) } + + var isAgreeToProcessData: Boolean + get() = getBoolean( + R.string.pref_key_ads_consent_data_processing, + R.bool.pref_default_ads_consent_data_processing + ) + set(value) = sharedPref.edit { + putBoolean(context.getString(R.string.pref_key_ads_consent_data_processing), value) + } + + var isPersonalizedAdsEnabled: Boolean + get() = sharedPref.getBoolean(PREF_KEY_PERSONALIZED_ADS_ENABLED, false) + set(value) = sharedPref.edit { putBoolean(PREF_KEY_PERSONALIZED_ADS_ENABLED, value) } + + val isAdsEnabledFlow = flowSharedPref.getBoolean( + context.getString(R.string.pref_key_ads_enabled), + context.resources.getBoolean(R.bool.pref_default_ads_enabled) + ).asFlow() + + var isAdsEnabled: Boolean + get() = getBoolean( + R.string.pref_key_ads_enabled, + R.bool.pref_default_ads_enabled + ) + set(value) = sharedPref.edit { + putBoolean(context.getString(R.string.pref_key_ads_enabled), value) + } private fun getLong(id: Int, default: Int) = getLong(context.getString(id), default) @@ -302,6 +344,10 @@ class PreferencesRepository @Inject constructor( private const val PREF_KEY_IN_APP_REVIEW_DONE = "in_app_review_done" + private const val PREF_KEY_APP_SUPPORT_SHOWN = "app_support_shown" + + private const val PREF_KEY_PERSONALIZED_ADS_ENABLED = "personalized_ads_enabled" + private const val PREF_KEY_ADMIN_DISMISSED_MESSAGE_IDS = "admin_message_dismissed_ids" } } 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 60e6f248f..e80f028e1 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/RecipientRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/RecipientRepository.kt @@ -1,10 +1,7 @@ package io.github.wulkanowy.data.repositories import io.github.wulkanowy.data.db.dao.RecipientDao -import io.github.wulkanowy.data.db.entities.Message -import io.github.wulkanowy.data.db.entities.Recipient -import io.github.wulkanowy.data.db.entities.ReportingUnit -import io.github.wulkanowy.data.db.entities.Student +import io.github.wulkanowy.data.db.entities.* import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.AutoRefreshHelper @@ -23,9 +20,10 @@ class RecipientRepository @Inject constructor( 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) + suspend fun refreshRecipients(student: Student, mailbox: Mailbox, type: MailboxType) { + val new = sdk.init(student).getRecipients(mailbox.globalKey) + .mapToEntities(mailbox.globalKey) + val old = recipientDb.loadAll(type, mailbox.globalKey) recipientDb.deleteAll(old uniqueSubtract new) recipientDb.insertAll(new uniqueSubtract old) @@ -33,18 +31,27 @@ class RecipientRepository @Inject constructor( refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, student)) } - suspend fun getRecipients(student: Student, unit: ReportingUnit, role: Int): List { - val cached = recipientDb.loadAll(unit.studentId, unit.unitId, role) + suspend fun getRecipients( + student: Student, + mailbox: Mailbox, + type: MailboxType + ): List { + val cached = recipientDb.loadAll(type, mailbox.globalKey) val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, student)) return if (cached.isEmpty() || isExpired) { - refreshRecipients(student, unit, role) - recipientDb.loadAll(unit.studentId, unit.unitId, role) + refreshRecipients(student, mailbox, type) + recipientDb.loadAll(type, mailbox.globalKey) } else cached } - suspend fun getMessageRecipients(student: Student, message: Message): List { - return sdk.init(student).getMessageRecipients(message.messageId, message.senderId) - .mapToEntities(student.studentId) - } + suspend fun getMessageSender( + student: Student, + mailbox: Mailbox, + message: Message + ): List = sdk.init(student) + .getMessageReplayDetails(message.messageGlobalKey) + .sender + .let(::listOf) + .mapToEntities(mailbox.globalKey) } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/ReportingUnitRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/ReportingUnitRepository.kt deleted file mode 100644 index b9caf978b..000000000 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/ReportingUnitRepository.kt +++ /dev/null @@ -1,42 +0,0 @@ -package io.github.wulkanowy.data.repositories - -import io.github.wulkanowy.data.db.dao.ReportingUnitDao -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.init -import io.github.wulkanowy.utils.uniqueSubtract -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class ReportingUnitRepository @Inject constructor( - private val reportingUnitDb: ReportingUnitDao, - private val sdk: Sdk -) { - - suspend fun refreshReportingUnits(student: Student) { - val new = sdk.init(student).getReportingUnits().mapToEntities(student) - val old = reportingUnitDb.load(student.id.toInt()) - - reportingUnitDb.deleteAll(old.uniqueSubtract(new)) - reportingUnitDb.insertAll(new.uniqueSubtract(old)) - } - - suspend fun getReportingUnits(student: Student): List { - return reportingUnitDb.load(student.id.toInt()).ifEmpty { - refreshReportingUnits(student) - - reportingUnitDb.load(student.id.toInt()) - } - } - - suspend fun getReportingUnit(student: Student, unitId: Int): ReportingUnit? { - return reportingUnitDb.loadOne(student.id.toInt(), unitId) ?: run { - refreshReportingUnits(student) - - return reportingUnitDb.loadOne(student.id.toInt(), unitId) - } - } -} 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 b6724ed34..4c42d092f 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 @@ -4,11 +4,11 @@ import io.github.wulkanowy.data.db.dao.SchoolAnnouncementDao import io.github.wulkanowy.data.db.entities.SchoolAnnouncement import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.mappers.mapToEntities +import io.github.wulkanowy.data.networkBoundResource import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.AutoRefreshHelper import io.github.wulkanowy.utils.getRefreshKey import io.github.wulkanowy.utils.init -import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.uniqueSubtract import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.sync.Mutex @@ -28,15 +28,17 @@ class SchoolAnnouncementRepository @Inject constructor( fun getSchoolAnnouncements( student: Student, - forceRefresh: Boolean, notify: Boolean = false + forceRefresh: Boolean, + notify: Boolean = false ) = networkBoundResource( mutex = saveFetchResultMutex, + isResultEmpty = { it.isEmpty() }, shouldFetch = { val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, student)) it.isEmpty() || forceRefresh || isExpired }, query = { - schoolAnnouncementDb.loadAll(student.studentId) + schoolAnnouncementDb.loadAll(student.userLoginId) }, fetch = { sdk.init(student) @@ -55,7 +57,7 @@ class SchoolAnnouncementRepository @Inject constructor( ) fun getSchoolAnnouncementFromDatabase(student: Student): Flow> { - return schoolAnnouncementDb.loadAll(student.studentId) + return schoolAnnouncementDb.loadAll(student.userLoginId) } suspend fun updateSchoolAnnouncement(schoolAnnouncement: List) = 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 288a1fb67..7972ed084 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 @@ -4,11 +4,11 @@ import io.github.wulkanowy.data.db.dao.SchoolDao import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.mappers.mapToEntity +import io.github.wulkanowy.data.networkBoundResource import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.AutoRefreshHelper import io.github.wulkanowy.utils.getRefreshKey import io.github.wulkanowy.utils.init -import io.github.wulkanowy.utils.networkBoundResource import kotlinx.coroutines.sync.Mutex import javax.inject.Inject import javax.inject.Singleton @@ -30,6 +30,7 @@ class SchoolRepository @Inject constructor( forceRefresh: Boolean, ) = networkBoundResource( mutex = saveFetchResultMutex, + isResultEmpty = { it == null }, shouldFetch = { val isExpired = refreshHelper.shouldBeRefreshed( key = getRefreshKey(cacheKey, student) @@ -38,7 +39,9 @@ class SchoolRepository @Inject constructor( }, query = { schoolDb.load(semester.studentId, semester.classId) }, fetch = { - sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear).getSchool() + sdk.init(student) + .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) + .getSchool() .mapToEntity(semester) }, saveFetchResult = { old, new -> 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 cc954558f..96f019223 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 @@ -5,11 +5,7 @@ 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.DispatchersProvider -import io.github.wulkanowy.utils.getCurrentOrLast -import io.github.wulkanowy.utils.init -import io.github.wulkanowy.utils.isCurrent -import io.github.wulkanowy.utils.uniqueSubtract +import io.github.wulkanowy.utils.* import kotlinx.coroutines.withContext import timber.log.Timber import javax.inject.Inject @@ -43,10 +39,14 @@ class SemesterRepository @Inject constructor( ): Boolean { val isNoSemesters = semesters.isEmpty() - val isRefreshOnModeChangeRequired = - if (Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API) { - semesters.firstOrNull { it.isCurrent }?.diaryId == 0 - } else false + val isRefreshOnModeChangeRequired = when { + Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API -> { + semesters.firstOrNull { it.isCurrent }?.let { + 0 == it.diaryId && 0 == it.kindergartenDiaryId + } == true + } + else -> false + } val isRefreshOnNoCurrentAppropriate = refreshOnNoCurrent && !semesters.any { semester -> semester.isCurrent } 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 e98daedf2..efc82a772 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 @@ -4,9 +4,9 @@ import io.github.wulkanowy.data.db.dao.StudentInfoDao import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.mappers.mapToEntity +import io.github.wulkanowy.data.networkBoundResource import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.init -import io.github.wulkanowy.utils.networkBoundResource import kotlinx.coroutines.sync.Mutex import javax.inject.Inject import javax.inject.Singleton @@ -25,10 +25,12 @@ class StudentInfoRepository @Inject constructor( forceRefresh: Boolean, ) = networkBoundResource( mutex = saveFetchResultMutex, + isResultEmpty = { it == null }, shouldFetch = { it == null || forceRefresh }, query = { studentInfoDao.loadStudentInfo(student.studentId) }, fetch = { - sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) + sdk.init(student) + .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) .getStudentInfo().mapToEntity(semester) }, saveFetchResult = { old, new -> diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt index 9e4a1aabc..f006b7d28 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 @@ -73,6 +73,15 @@ class StudentRepository @Inject constructor( } } + suspend fun getSavedStudentById(id: Long, decryptPass: Boolean = true) = + studentDb.loadStudentWithSemestersById(id)?.apply { + if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API) { + student.password = withContext(dispatchers.io) { + decrypt(student.password) + } + } + } + suspend fun getStudentById(id: Long, decryptPass: Boolean = true): Student { val student = studentDb.loadById(id) ?: throw NoCurrentStudentException() @@ -128,4 +137,7 @@ class StudentRepository @Inject constructor( suspend fun updateStudentNickAndAvatar(studentNickAndAvatar: StudentNickAndAvatar) = studentDb.update(studentNickAndAvatar) + + suspend fun isOneUniqueStudent() = getSavedStudents(false) + .distinctBy { it.student.studentName }.size == 1 } 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 d81cb7c92..3926122b3 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 @@ -4,11 +4,11 @@ import io.github.wulkanowy.data.db.dao.SubjectDao import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.mappers.mapToEntities +import io.github.wulkanowy.data.networkBoundResource import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.AutoRefreshHelper import io.github.wulkanowy.utils.getRefreshKey import io.github.wulkanowy.utils.init -import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.uniqueSubtract import kotlinx.coroutines.sync.Mutex import javax.inject.Inject @@ -31,13 +31,15 @@ class SubjectRepository @Inject constructor( forceRefresh: Boolean = false, ) = networkBoundResource( mutex = saveFetchResultMutex, + isResultEmpty = { it.isEmpty() }, 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) + sdk.init(student) + .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) .getSubjects().mapToEntities(semester) }, saveFetchResult = { old, new -> diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/TeacherRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/TeacherRepository.kt index 029b2707a..acd71e1f4 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 @@ -4,11 +4,11 @@ import io.github.wulkanowy.data.db.dao.TeacherDao import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.mappers.mapToEntities +import io.github.wulkanowy.data.networkBoundResource import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.AutoRefreshHelper import io.github.wulkanowy.utils.getRefreshKey import io.github.wulkanowy.utils.init -import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.uniqueSubtract import kotlinx.coroutines.sync.Mutex import javax.inject.Inject @@ -31,13 +31,15 @@ class TeacherRepository @Inject constructor( forceRefresh: Boolean, ) = networkBoundResource( mutex = saveFetchResultMutex, + isResultEmpty = { it.isEmpty() }, 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) + sdk.init(student) + .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) .getTeachers(semester.semesterId) .mapToEntities(semester) }, diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/TimetableRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/TimetableRepository.kt index 8be621122..3145c2a23 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/TimetableRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/TimetableRepository.kt @@ -3,22 +3,13 @@ package io.github.wulkanowy.data.repositories import io.github.wulkanowy.data.db.dao.TimetableAdditionalDao import io.github.wulkanowy.data.db.dao.TimetableDao import io.github.wulkanowy.data.db.dao.TimetableHeaderDao -import io.github.wulkanowy.data.db.entities.Semester -import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.data.db.entities.Timetable -import io.github.wulkanowy.data.db.entities.TimetableAdditional -import io.github.wulkanowy.data.db.entities.TimetableHeader +import io.github.wulkanowy.data.db.entities.* import io.github.wulkanowy.data.mappers.mapToEntities +import io.github.wulkanowy.data.networkBoundResource import io.github.wulkanowy.data.pojos.TimetableFull import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.services.alarm.TimetableNotificationSchedulerHelper -import io.github.wulkanowy.utils.AutoRefreshHelper -import io.github.wulkanowy.utils.getRefreshKey -import io.github.wulkanowy.utils.init -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 io.github.wulkanowy.utils.* import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.sync.Mutex @@ -40,6 +31,10 @@ class TimetableRepository @Inject constructor( private val cacheKey = "timetable" + enum class TimetableType { + NORMAL, ADDITIONAL + } + fun getTimetable( student: Student, semester: Semester, @@ -47,9 +42,16 @@ class TimetableRepository @Inject constructor( end: LocalDate, forceRefresh: Boolean, refreshAdditional: Boolean = false, - notify: Boolean = false + notify: Boolean = false, + timetableType: TimetableType = TimetableType.NORMAL ) = networkBoundResource( mutex = saveFetchResultMutex, + isResultEmpty = { + when (timetableType) { + TimetableType.NORMAL -> it.lessons.isEmpty() + TimetableType.ADDITIONAL -> it.additional.isEmpty() + } + }, shouldFetch = { (timetable, additional, headers) -> val refreshKey = getRefreshKey(cacheKey, semester, start, end) val isExpired = refreshHelper.shouldBeRefreshed(refreshKey) @@ -62,7 +64,7 @@ class TimetableRepository @Inject constructor( query = { getFullTimetableFromDatabase(student, semester, start, end) }, fetch = { val timetableFull = sdk.init(student) - .switchDiary(semester.diaryId, semester.schoolYear) + .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) .getTimetableFull(start.monday, end.sunday) timetableFull.mapToEntities(semester) @@ -152,7 +154,8 @@ class TimetableRepository @Inject constructor( old: List, new: List ) { - timetableAdditionalDb.deleteAll(old uniqueSubtract new) + val oldFiltered = old.filter { !it.isAddedByUser } + timetableAdditionalDb.deleteAll(oldFiltered uniqueSubtract new) timetableAdditionalDb.insertAll(new uniqueSubtract old) } @@ -160,4 +163,14 @@ class TimetableRepository @Inject constructor( timetableHeaderDb.deleteAll(old uniqueSubtract new) timetableHeaderDb.insertAll(new uniqueSubtract old) } + + suspend fun saveAdditionalList(additionalList: List) = + timetableAdditionalDb.insertAll(additionalList) + + suspend fun deleteAdditional(additional: TimetableAdditional, deleteSeries: Boolean) = + if (deleteSeries) { + timetableAdditionalDb.deleteAllByRepeatId(additional.repeatId!!) + } else { + timetableAdditionalDb.deleteAll(listOf(additional)) + } } diff --git a/app/src/main/java/io/github/wulkanowy/data/serializers/LocalDateSerializer.kt b/app/src/main/java/io/github/wulkanowy/data/serializers/LocalDateSerializer.kt new file mode 100644 index 000000000..ba97d37a2 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/serializers/LocalDateSerializer.kt @@ -0,0 +1,32 @@ +package io.github.wulkanowy.data.serializers + +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.nullable +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import java.time.LocalDate + +@OptIn(ExperimentalSerializationApi::class) +object LocalDateSerializer : KSerializer { + + override val descriptor = PrimitiveSerialDescriptor("LocalDate", PrimitiveKind.LONG).nullable + + override fun serialize(encoder: Encoder, value: LocalDate?) { + if (value == null) { + encoder.encodeNull() + } else { + encoder.encodeNotNullMark() + encoder.encodeLong(value.toEpochDay()) + } + } + + override fun deserialize(decoder: Decoder): LocalDate? = + if (decoder.decodeNotNullMark()) { + LocalDate.ofEpochDay(decoder.decodeLong()) + } else { + decoder.decodeNull() + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/services/HiltBroadcastReceiver.kt b/app/src/main/java/io/github/wulkanowy/services/HiltBroadcastReceiver.kt deleted file mode 100644 index 1e795d439..000000000 --- a/app/src/main/java/io/github/wulkanowy/services/HiltBroadcastReceiver.kt +++ /dev/null @@ -1,9 +0,0 @@ -package io.github.wulkanowy.services - -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent - -abstract class HiltBroadcastReceiver : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) {} -} 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 b388d2ac5..01a583e13 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,6 +1,7 @@ package io.github.wulkanowy.services.alarm import android.app.PendingIntent +import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.os.Build @@ -9,26 +10,23 @@ import androidx.core.app.NotificationCompat 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.onResourceError import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.StudentRepository -import io.github.wulkanowy.services.HiltBroadcastReceiver +import io.github.wulkanowy.data.resourceFlow import io.github.wulkanowy.services.sync.channels.UpcomingLessonsChannel.Companion.CHANNEL_ID 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 import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach import timber.log.Timber import javax.inject.Inject @AndroidEntryPoint -class TimetableNotificationReceiver : HiltBroadcastReceiver() { +class TimetableNotificationReceiver : BroadcastReceiver() { @Inject lateinit var studentRepository: StudentRepository @@ -41,6 +39,8 @@ class TimetableNotificationReceiver : HiltBroadcastReceiver() { const val NOTIFICATION_TYPE_UPCOMING = 2 const val NOTIFICATION_TYPE_LAST_LESSON_CANCELLATION = 3 + // FIXME only shows one notification even if there are multiple students. + // Probably want to fix after #721 is merged. const val NOTIFICATION_ID = 2137 const val STUDENT_NAME = "student_name" @@ -56,20 +56,24 @@ class TimetableNotificationReceiver : HiltBroadcastReceiver() { @OptIn(DelicateCoroutinesApi::class) override fun onReceive(context: Context, intent: Intent) { - super.onReceive(context, intent) Timber.d("Receiving intent... ${intent.toUri(0)}") - flowWithResource { + resourceFlow { + val showStudentName = !studentRepository.isOneUniqueStudent() val student = studentRepository.getCurrentStudent(false) val studentId = intent.getIntExtra(STUDENT_ID, 0) - if (student.studentId == studentId) prepareNotification(context, intent) - else Timber.d("Notification studentId($studentId) differs from current(${student.studentId})") - }.onEach { - if (it.status == Status.ERROR) Timber.e(it.error!!) - }.launchIn(GlobalScope) + + if (student.studentId == studentId) { + prepareNotification(context, intent, showStudentName) + } else { + Timber.d("Notification studentId($studentId) differs from current(${student.studentId})") + } + } + .onResourceError { Timber.e(it) } + .launchIn(GlobalScope) } - private fun prepareNotification(context: Context, intent: Intent) { + private fun prepareNotification(context: Context, intent: Intent, showStudentName: Boolean) { val type = intent.getIntExtra(LESSON_TYPE, 0) val isPersistent = preferencesRepository.isUpcomingLessonsNotificationsPersistent @@ -78,7 +82,7 @@ class TimetableNotificationReceiver : HiltBroadcastReceiver() { } val studentId = intent.getIntExtra(STUDENT_ID, 0) - val studentName = intent.getStringExtra(STUDENT_NAME) + val studentName = intent.getStringExtra(STUDENT_NAME).takeIf { showStudentName } val subject = intent.getStringExtra(LESSON_TITLE) val room = intent.getStringExtra(LESSON_ROOM) @@ -89,21 +93,28 @@ class TimetableNotificationReceiver : HiltBroadcastReceiver() { val nextSubject = intent.getStringExtra(LESSON_NEXT_TITLE) val nextRoom = intent.getStringExtra(LESSON_NEXT_ROOM) - Timber.d("TimetableNotification receive: type: $type, subject: $subject, start: ${start.toLocalDateTime()}, student: $studentId") + Timber.d("TimetableNotification receive: type: $type, subject: $subject, start: $start, student: $studentId") + + val notificationTitleResId = + if (type == NOTIFICATION_TYPE_CURRENT) R.string.timetable_now else R.string.timetable_next + val notificationTitle = + context.getString(notificationTitleResId, "($room) $subject".removePrefix("()")) + + val nextLessonText = nextSubject?.let { + context.getString( + R.string.timetable_later, + "($nextRoom) $nextSubject".removePrefix("()") + ) + } 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 = context, + isPersistent = isPersistent, + studentName = studentName, + countDown = if (type == NOTIFICATION_TYPE_CURRENT) end else start, + timeout = end - start, + title = notificationTitle, + next = nextLessonText ) } @@ -130,10 +141,11 @@ class TimetableNotificationReceiver : HiltBroadcastReceiver() { .setTimeoutAfter(timeout) .setSmallIcon(R.drawable.ic_stat_timetable) .setColor(context.getCompatColor(R.color.colorPrimary)) - .setStyle(NotificationCompat.InboxStyle().also { - it.setSummaryText(studentName) - it.addLine(next) - }) + .setStyle(NotificationCompat.InboxStyle() + .addLine(next) + .also { inboxStyle -> + studentName?.let { inboxStyle.setSummaryText(it) } + }) .setContentIntent( PendingIntent.getActivity( context, 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 dc9b8f1da..42078d03f 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 @@ -28,12 +28,12 @@ import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companio 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.Duration.ofMinutes +import java.time.Instant +import java.time.Instant.now import java.time.LocalDate -import java.time.LocalDateTime -import java.time.LocalDateTime.now import javax.inject.Inject class TimetableNotificationSchedulerHelper @Inject constructor( @@ -43,14 +43,14 @@ class TimetableNotificationSchedulerHelper @Inject constructor( private val dispatchersProvider: DispatchersProvider, ) { - private fun getRequestCode(time: LocalDateTime, studentId: Int) = - (time.toTimestamp() * studentId).toInt() + private fun getRequestCode(time: Instant, studentId: Int): Int = + (time.toEpochMilli() * studentId).toInt() private fun getUpcomingLessonTime( index: Int, day: List, lesson: Timetable - ) = day.getOrNull(index - 1)?.end ?: lesson.start.minusMinutes(30) + ): Instant = day.getOrNull(index - 1)?.end ?: lesson.start.minus(ofMinutes(30)) suspend fun cancelScheduled(lessons: List, student: Student) { val studentId = student.studentId @@ -71,7 +71,7 @@ class TimetableNotificationSchedulerHelper @Inject constructor( } } - private fun cancelScheduledTo(range: ClosedRange, requestCode: Int) { + private fun cancelScheduledTo(range: ClosedRange, requestCode: Int) { if (now() in range) cancelNotification() alarmManager.cancel( @@ -150,8 +150,8 @@ class TimetableNotificationSchedulerHelper @Inject constructor( putExtra(STUDENT_ID, student.studentId) putExtra(STUDENT_NAME, student.nickOrName) putExtra(LESSON_ROOM, lesson.room) - putExtra(LESSON_START, lesson.start.toTimestamp()) - putExtra(LESSON_END, lesson.end.toTimestamp()) + putExtra(LESSON_START, lesson.start.toEpochMilli()) + putExtra(LESSON_END, lesson.end.toEpochMilli()) putExtra(LESSON_TITLE, lesson.subject) putExtra(LESSON_NEXT_TITLE, nextLesson?.subject) putExtra(LESSON_NEXT_ROOM, nextLesson?.room) @@ -162,11 +162,11 @@ class TimetableNotificationSchedulerHelper @Inject constructor( intent: Intent, studentId: Int, notificationType: Int, - time: LocalDateTime + time: Instant ) { try { AlarmManagerCompat.setExactAndAllowWhileIdle( - alarmManager, RTC_WAKEUP, time.toTimestamp(), + alarmManager, RTC_WAKEUP, time.toEpochMilli(), PendingIntent.getBroadcast(context, getRequestCode(time, studentId), intent.also { it.putExtra(LESSON_TYPE, notificationType) }, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE) 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 index c7df2dbc1..3c173495a 100644 --- a/app/src/main/java/io/github/wulkanowy/services/piggyback/VulcanNotificationListenerService.kt +++ b/app/src/main/java/io/github/wulkanowy/services/piggyback/VulcanNotificationListenerService.kt @@ -19,6 +19,9 @@ class VulcanNotificationListenerService : NotificationListenerService() { override fun onNotificationPosted(statusBarNotification: StatusBarNotification?) { if (statusBarNotification?.packageName == "pl.edu.vulcan.hebe" && preferenceRepository.isNotificationPiggybackEnabled) { syncManager.startOneTimeSyncWorker() + if (preferenceRepository.isNotificationPiggybackRemoveOriginalEnabled) { + cancelNotification(statusBarNotification.key) + } } } } \ 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 index 4ad9ac120..5e59aa54b 100644 --- a/app/src/main/java/io/github/wulkanowy/services/shortcuts/ShortcutsHelper.kt +++ b/app/src/main/java/io/github/wulkanowy/services/shortcuts/ShortcutsHelper.kt @@ -15,76 +15,41 @@ 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() { + 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") - } - ) + .setIntent(SplashActivity.getStartIntent(context, Destination.Grade) + .apply { action = Intent.ACTION_VIEW }) .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") - } - ) + .setIntent(SplashActivity.getStartIntent(context, Destination.Attendance) + .apply { action = Intent.ACTION_VIEW }) .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") - } - ) + .setIntent(SplashActivity.getStartIntent(context, Destination.Exam) + .apply { action = Intent.ACTION_VIEW }) .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") - } - ) + .setIntent(SplashActivity.getStartIntent(context, Destination.Timetable()) + .apply { action = Intent.ACTION_VIEW }) .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 32ca20afc..c1bed4dd3 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 @@ -74,10 +74,12 @@ class SyncManager @Inject constructor( } } - fun startOneTimeSyncWorker(): Flow { + // if quiet, no notifications will be sent + fun startOneTimeSyncWorker(quiet: Boolean = false): Flow { val work = OneTimeWorkRequestBuilder() .setInputData( Data.Builder() + .putBoolean("quiet", quiet) .putBoolean("one_time", true) .build() ) 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 52979e635..5dddd9a78 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 @@ -23,7 +23,7 @@ import io.github.wulkanowy.utils.DispatchersProvider import io.github.wulkanowy.utils.getCompatColor import kotlinx.coroutines.withContext import timber.log.Timber -import java.time.LocalDateTime +import java.time.Instant import kotlin.random.Random @HiltWorker @@ -38,18 +38,23 @@ class SyncWorker @AssistedInject constructor( private val dispatchersProvider: DispatchersProvider ) : CoroutineWorker(appContext, workerParameters) { - override suspend fun doWork() = withContext(dispatchersProvider.io) { + override suspend fun doWork(): Result = withContext(dispatchersProvider.io) { Timber.i("SyncWorker is starting") if (!studentRepository.isCurrentStudentSet()) return@withContext Result.failure() - val student = studentRepository.getCurrentStudent() - val semester = semesterRepository.getCurrentSemester(student, true) + val (student, semester) = try { + val student = studentRepository.getCurrentStudent() + val semester = semesterRepository.getCurrentSemester(student, true) + student to semester + } catch (e: Throwable) { + return@withContext getResultFromErrors(listOf(e)) + } val exceptions = works.mapNotNull { work -> try { Timber.i("${work::class.java.simpleName} is starting") - work.doWork(student, semester) + work.doWork(student, semester, isNotificationsEnabled()) Timber.i("${work::class.java.simpleName} result: Success") null } catch (e: Throwable) { @@ -62,20 +67,7 @@ class SyncWorker @AssistedInject constructor( } } } - val result = when { - exceptions.isNotEmpty() && inputData.getBoolean("one_time", false) -> { - Result.failure( - Data.Builder() - .putString("error", exceptions.map { it.stackTraceToString() }.toString()) - .build() - ) - } - exceptions.isNotEmpty() -> Result.retry() - else -> { - preferencesRepository.lasSyncDate = LocalDateTime.now() - Result.success() - } - } + val result = getResultFromErrors(exceptions) if (preferencesRepository.isDebugNotificationEnable) notify(result) Timber.i("SyncWorker result: $result") @@ -83,6 +75,27 @@ class SyncWorker @AssistedInject constructor( return@withContext result } + private fun isNotificationsEnabled(): Boolean { + val quiet = inputData.getBoolean("quiet", false) + return preferencesRepository.isNotificationsEnable && !quiet + } + + private fun getResultFromErrors(errors: List): Result = when { + errors.isNotEmpty() && inputData.getBoolean("one_time", false) -> { + Result.failure( + Data.Builder() + .putString("error_message", errors.joinToString { it.message.toString() }) + .putString("error_stack", errors.map { it.stackTraceToString() }.toString()) + .build() + ) + } + errors.isNotEmpty() -> Result.retry() + else -> { + preferencesRepository.lasSyncDate = Instant.now() + Result.success() + } + } + private fun notify(result: Result) { notificationManager.notify( Random.nextInt(Int.MAX_VALUE), 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 index da8d094c6..dadb68c50 100644 --- 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 @@ -14,11 +14,12 @@ 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.ui.modules.splash.SplashActivity 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 java.time.Instant import javax.inject.Inject import kotlin.random.Random @@ -47,7 +48,7 @@ class AppNotificationManager @Inject constructor( PendingIntent.getActivity( context, Random.nextInt(), - notificationData.intentToStart, + SplashActivity.getStartIntent(context, notificationData.destination), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE ) ) @@ -57,7 +58,7 @@ class AppNotificationManager @Inject constructor( NotificationCompat.BigTextStyle() .bigText(notificationData.content) .also { builder -> - if (shouldShowStudentName()) { + if (!studentRepository.isOneUniqueStudent()) { builder.setSummaryText(student.nickOrName) } } @@ -92,7 +93,7 @@ class AppNotificationManager @Inject constructor( PendingIntent.getActivity( context, Random.nextInt(), - notificationData.intentToStart, + SplashActivity.getStartIntent(context, notificationData.destination), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE ) ) @@ -102,7 +103,7 @@ class AppNotificationManager @Inject constructor( NotificationCompat.BigTextStyle() .bigText(notificationData.content) .also { builder -> - if (shouldShowStudentName()) { + if (!studentRepository.isOneUniqueStudent()) { builder.setSummaryText(student.nickOrName) } } @@ -134,7 +135,7 @@ class AppNotificationManager @Inject constructor( .setStyle( NotificationCompat.InboxStyle() .also { builder -> - if (shouldShowStudentName()) { + if (!studentRepository.isOneUniqueStudent()) { builder.setSummaryText(student.nickOrName) } groupNotificationData.notificationDataList.forEach { @@ -146,7 +147,7 @@ class AppNotificationManager @Inject constructor( PendingIntent.getActivity( context, Random.nextInt(), - groupNotificationData.intentToStart, + SplashActivity.getStartIntent(context, groupNotificationData.destination), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE ) ) @@ -168,13 +169,11 @@ class AppNotificationManager @Inject constructor( studentId = student.id, title = notificationData.title, content = notificationData.content, + destination = notificationData.destination, type = notificationType, - date = LocalDateTime.now() + date = Instant.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/ChangeTimetableNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/ChangeTimetableNotification.kt index 7bfef96a4..43ae1fea9 100644 --- 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 @@ -8,11 +8,10 @@ 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.Instant import java.time.LocalDate -import java.time.LocalDateTime import javax.inject.Inject class ChangeTimetableNotification @Inject constructor( @@ -21,10 +20,11 @@ class ChangeTimetableNotification @Inject constructor( ) { suspend fun notify(items: List, student: Student) { - val currentTime = LocalDateTime.now() + val currentTime = Instant.now() val changedLessons = items.filter { (it.canceled || it.changes) && it.start > currentTime } - val notificationDataList = changedLessons.groupBy { it.date } - .map { (date, lessons) -> + val lessonsByDate = changedLessons.groupBy { it.date } + val notificationDataList = lessonsByDate + .flatMap { (date, lessons) -> getNotificationContents(date, lessons).map { NotificationData( title = context.getPlural( @@ -32,14 +32,10 @@ class ChangeTimetableNotification @Inject constructor( 1 ), content = it, - intentToStart = SplashActivity.getStartIntent( - context = context, - destination = Destination.Timetable(date) - ) + destination = Destination.Timetable(date) ) } } - .flatten() .ifEmpty { return } val groupNotificationData = GroupNotificationData( @@ -53,7 +49,7 @@ class ChangeTimetableNotification @Inject constructor( changedLessons.size, changedLessons.size ), - intentToStart = SplashActivity.getStartIntent(context, Destination.Timetable()), + destination = Destination.Timetable(lessonsByDate.toSortedMap().firstKey()), type = NotificationType.CHANGE_TIMETABLE ) 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 index c78dcd053..99473a8ec 100644 --- 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 @@ -8,7 +8,6 @@ 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 @@ -22,8 +21,9 @@ class NewAttendanceNotification @Inject constructor( suspend fun notify(items: List, student: Student) { val lines = items.filterNot { it.presence || it.name == "UNKNOWN" } .map { + val lesson = it.subject.ifBlank { "Lekcja ${it.number}" } val description = context.getString(it.descriptionRes) - "${it.date.toFormattedString("dd.MM")} - ${it.subject}: $description" + "${it.date.toFormattedString("dd.MM")} - $lesson: $description" } .ifEmpty { return } @@ -31,7 +31,7 @@ class NewAttendanceNotification @Inject constructor( NotificationData( title = context.getPlural(R.plurals.attendance_notify_new_items_title, 1), content = it, - intentToStart = SplashActivity.getStartIntent(context, Destination.Attendance) + destination = Destination.Attendance ) } @@ -46,7 +46,7 @@ class NewAttendanceNotification @Inject constructor( notificationDataList.size, notificationDataList.size ), - intentToStart = SplashActivity.getStartIntent(context, Destination.Attendance), + destination = Destination.Attendance, type = NotificationType.NEW_ATTENDANCE ) 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 8ef147886..92977ebb1 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 @@ -11,7 +11,7 @@ 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 java.time.Instant import javax.inject.Inject class NewConferenceNotification @Inject constructor( @@ -20,7 +20,7 @@ class NewConferenceNotification @Inject constructor( ) { suspend fun notify(items: List, student: Student) { - val today = LocalDateTime.now() + val today = Instant.now() val lines = items.filter { !it.date.isBefore(today) } .map { "${it.date.toFormattedString("dd.MM")} - ${it.title}: ${it.subject}" @@ -31,7 +31,7 @@ class NewConferenceNotification @Inject constructor( NotificationData( title = context.getPlural(R.plurals.conference_notify_new_item_title, 1), content = it, - intentToStart = SplashActivity.getStartIntent(context, Destination.Conference) + destination = Destination.Conference ) } @@ -43,7 +43,7 @@ class NewConferenceNotification @Inject constructor( lines.size, lines.size ), - intentToStart = SplashActivity.getStartIntent(context, Destination.Conference), + destination = Destination.Conference, type = NotificationType.NEW_CONFERENCE ) 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 b3cf04c41..125bbf92d 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 @@ -31,7 +31,7 @@ class NewExamNotification @Inject constructor( NotificationData( title = context.getPlural(R.plurals.exam_notify_new_item_title, 1), content = it, - intentToStart = SplashActivity.getStartIntent(context, Destination.Exam), + destination = Destination.Exam, ) } @@ -43,7 +43,7 @@ class NewExamNotification @Inject constructor( lines.size, lines.size ), - intentToStart = SplashActivity.getStartIntent(context, Destination.Exam), + destination = Destination.Exam, type = NotificationType.NEW_EXAM ) 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 39ecbe33d..9b49ed178 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 @@ -22,8 +22,11 @@ class NewGradeNotification @Inject constructor( 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), + content = buildString { + append("${it.subject}: ${it.entry}") + if (it.comment.isNotBlank()) append(" (${it.comment})") + }, + destination = Destination.Grade, ) } @@ -31,7 +34,7 @@ class NewGradeNotification @Inject constructor( 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), + destination = Destination.Grade, type = NotificationType.NEW_GRADE_DETAILS ) @@ -43,7 +46,7 @@ class NewGradeNotification @Inject constructor( NotificationData( title = context.getPlural(R.plurals.grade_new_items_predicted, 1), content = "${it.subject}: ${it.predictedGrade}", - intentToStart = SplashActivity.getStartIntent(context, Destination.Grade), + destination = Destination.Grade, ) } @@ -55,7 +58,7 @@ class NewGradeNotification @Inject constructor( items.size, items.size ), - intentToStart = SplashActivity.getStartIntent(context, Destination.Grade), + destination = Destination.Grade, type = NotificationType.NEW_GRADE_PREDICTED ) @@ -67,7 +70,7 @@ class NewGradeNotification @Inject constructor( NotificationData( title = context.getPlural(R.plurals.grade_new_items_final, 1), content = "${it.subject}: ${it.finalGrade}", - intentToStart = SplashActivity.getStartIntent(context, Destination.Grade), + destination = Destination.Grade, ) } @@ -79,7 +82,7 @@ class NewGradeNotification @Inject constructor( items.size, items.size ), - intentToStart = SplashActivity.getStartIntent(context, Destination.Grade), + destination = Destination.Grade, type = NotificationType.NEW_GRADE_FINAL ) 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 ff32aa66f..856c51581 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 @@ -31,7 +31,7 @@ class NewHomeworkNotification @Inject constructor( NotificationData( title = context.getPlural(R.plurals.homework_notify_new_item_title, 1), content = it, - intentToStart = SplashActivity.getStartIntent(context, Destination.Homework), + destination = Destination.Homework, ) } @@ -42,7 +42,7 @@ class NewHomeworkNotification @Inject constructor( lines.size, lines.size ), - intentToStart = SplashActivity.getStartIntent(context, Destination.Homework), + destination = Destination.Homework, type = NotificationType.NEW_HOMEWORK, notificationDataList = notificationDataList ) 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 5c36a06c0..bbe9b8a18 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 @@ -22,7 +22,7 @@ class NewLuckyNumberNotification @Inject constructor( R.string.lucky_number_notify_new_item, item.luckyNumber.toString() ), - intentToStart = SplashActivity.getStartIntent(context, Destination.LuckyNumber) + destination = Destination.LuckyNumber ) appNotificationManager.sendSingleNotification( 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 b98d34668..3b7bcff05 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 @@ -21,8 +21,8 @@ class NewMessageNotification @Inject constructor( 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), + content = "${it.correspondents}: ${it.subject}", + destination = Destination.Message, ) } @@ -30,7 +30,7 @@ class NewMessageNotification @Inject constructor( 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), + destination = Destination.Message, type = NotificationType.NEW_MESSAGE ) 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 65520e01b..dae7d4330 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 @@ -29,13 +29,13 @@ class NewNoteNotification @Inject constructor( NotificationData( title = context.getPlural(titleRes, 1), content = "${it.teacher}: ${it.category}", - intentToStart = SplashActivity.getStartIntent(context, Destination.Note), + destination = Destination.Note, ) } val groupNotificationData = GroupNotificationData( notificationDataList = notificationDataList, - intentToStart = SplashActivity.getStartIntent(context, Destination.Note), + destination = 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 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 6b839d298..cc7e46564 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,6 +1,7 @@ package io.github.wulkanowy.services.sync.notifications import android.content.Context +import androidx.core.text.parseAsHtml import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.SchoolAnnouncement @@ -20,23 +21,17 @@ class NewSchoolAnnouncementNotification @Inject constructor( suspend fun notify(items: List, student: Student) { val notificationDataList = items.map { NotificationData( - intentToStart = SplashActivity.getStartIntent( - context = context, - destination = Destination.SchoolAnnouncement - ), + destination = Destination.SchoolAnnouncement, title = context.getPlural( R.plurals.school_announcement_notify_new_item_title, 1 ), - content = "${it.subject}: ${it.content}" + content = "${it.subject}: ${it.content.parseAsHtml()}" ) } val groupNotificationData = GroupNotificationData( type = NotificationType.NEW_ANNOUNCEMENT, - intentToStart = SplashActivity.getStartIntent( - context = context, - destination = Destination.SchoolAnnouncement - ), + destination = Destination.SchoolAnnouncement, title = context.getPlural( R.plurals.school_announcement_notify_new_item_title, items.size diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/works/AttendanceSummaryWork.kt b/app/src/main/java/io/github/wulkanowy/services/sync/works/AttendanceSummaryWork.kt index cbe1fe6bd..55ce7e908 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/works/AttendanceSummaryWork.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/works/AttendanceSummaryWork.kt @@ -3,14 +3,19 @@ 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.AttendanceSummaryRepository -import io.github.wulkanowy.utils.waitForResult +import io.github.wulkanowy.data.waitForResult import javax.inject.Inject class AttendanceSummaryWork @Inject constructor( private val attendanceSummaryRepository: AttendanceSummaryRepository ) : Work { - override suspend fun doWork(student: Student, semester: Semester) { - attendanceSummaryRepository.getAttendanceSummary(student, semester, -1, true).waitForResult() + override suspend fun doWork(student: Student, semester: Semester, notify: Boolean) { + attendanceSummaryRepository.getAttendanceSummary( + student = student, + semester = semester, + subjectId = -1, + forceRefresh = true, + ).waitForResult() } } 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 f7b680e31..657f69638 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,10 +3,9 @@ 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.data.repositories.PreferencesRepository +import io.github.wulkanowy.data.waitForResult 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 @@ -14,17 +13,16 @@ import javax.inject.Inject 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) { + override suspend fun doWork(student: Student, semester: Semester, notify: Boolean) { attendanceRepository.getAttendance( student = student, semester = semester, start = now().previousOrSameSchoolDay, end = now().previousOrSameSchoolDay, forceRefresh = true, - notify = preferencesRepository.isNotificationsEnable + notify = notify, ) .waitForResult() diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/works/CompletedLessonWork.kt b/app/src/main/java/io/github/wulkanowy/services/sync/works/CompletedLessonWork.kt index 17bd61292..f898aa04b 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/works/CompletedLessonWork.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/works/CompletedLessonWork.kt @@ -3,9 +3,9 @@ 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.CompletedLessonsRepository +import io.github.wulkanowy.data.waitForResult import io.github.wulkanowy.utils.monday import io.github.wulkanowy.utils.sunday -import io.github.wulkanowy.utils.waitForResult import java.time.LocalDate.now import javax.inject.Inject @@ -13,7 +13,13 @@ class CompletedLessonWork @Inject constructor( private val completedLessonsRepository: CompletedLessonsRepository ) : Work { - override suspend fun doWork(student: Student, semester: Semester) { - completedLessonsRepository.getCompletedLessons(student, semester, now().monday, now().sunday, true).waitForResult() + override suspend fun doWork(student: Student, semester: Semester, notify: Boolean) { + completedLessonsRepository.getCompletedLessons( + student = student, + semester = semester, + start = now().monday, + end = now().sunday, + forceRefresh = true, + ).waitForResult() } } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/works/ConferenceWork.kt b/app/src/main/java/io/github/wulkanowy/services/sync/works/ConferenceWork.kt index 002b4f764..c85c00433 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/works/ConferenceWork.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/works/ConferenceWork.kt @@ -3,24 +3,22 @@ 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.ConferenceRepository -import io.github.wulkanowy.data.repositories.PreferencesRepository +import io.github.wulkanowy.data.waitForResult import io.github.wulkanowy.services.sync.notifications.NewConferenceNotification -import io.github.wulkanowy.utils.waitForResult import kotlinx.coroutines.flow.first import javax.inject.Inject class ConferenceWork @Inject constructor( private val conferenceRepository: ConferenceRepository, - private val preferencesRepository: PreferencesRepository, private val newConferenceNotification: NewConferenceNotification, ) : Work { - override suspend fun doWork(student: Student, semester: Semester) { + override suspend fun doWork(student: Student, semester: Semester, notify: Boolean) { conferenceRepository.getConferences( student = student, semester = semester, forceRefresh = true, - notify = preferencesRepository.isNotificationsEnable + notify = notify ).waitForResult() conferenceRepository.getConferenceFromDatabase(semester).first() diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/works/ExamWork.kt b/app/src/main/java/io/github/wulkanowy/services/sync/works/ExamWork.kt index a1ce553a7..7071bce20 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/works/ExamWork.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/works/ExamWork.kt @@ -3,27 +3,25 @@ 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.ExamRepository -import io.github.wulkanowy.data.repositories.PreferencesRepository +import io.github.wulkanowy.data.waitForResult import io.github.wulkanowy.services.sync.notifications.NewExamNotification -import io.github.wulkanowy.utils.waitForResult import kotlinx.coroutines.flow.first import java.time.LocalDate.now import javax.inject.Inject class ExamWork @Inject constructor( private val examRepository: ExamRepository, - private val preferencesRepository: PreferencesRepository, private val newExamNotification: NewExamNotification, ) : Work { - override suspend fun doWork(student: Student, semester: Semester) { + override suspend fun doWork(student: Student, semester: Semester, notify: Boolean) { examRepository.getExams( student = student, semester = semester, start = now(), end = now(), forceRefresh = true, - notify = preferencesRepository.isNotificationsEnable + notify = notify, ).waitForResult() examRepository.getExamsFromDatabase(semester, now()).first() diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/works/GradeStatisticsWork.kt b/app/src/main/java/io/github/wulkanowy/services/sync/works/GradeStatisticsWork.kt index 4575b419b..ac35bc9a8 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/works/GradeStatisticsWork.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/works/GradeStatisticsWork.kt @@ -3,14 +3,15 @@ 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.GradeStatisticsRepository -import io.github.wulkanowy.utils.waitForResult +import io.github.wulkanowy.data.waitForResult + import javax.inject.Inject class GradeStatisticsWork @Inject constructor( private val gradeStatisticsRepository: GradeStatisticsRepository ) : Work { - override suspend fun doWork(student: Student, semester: Semester) { + override suspend fun doWork(student: Student, semester: Semester, notify: Boolean) { with(gradeStatisticsRepository) { getGradesPartialStatistics(student, semester, "Wszystkie", forceRefresh = true).waitForResult() getGradesSemesterStatistics(student, semester, "Wszystkie", forceRefresh = true).waitForResult() diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/works/GradeWork.kt b/app/src/main/java/io/github/wulkanowy/services/sync/works/GradeWork.kt index 0932405eb..ba21b8600 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/works/GradeWork.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/works/GradeWork.kt @@ -3,24 +3,22 @@ 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.GradeRepository -import io.github.wulkanowy.data.repositories.PreferencesRepository +import io.github.wulkanowy.data.waitForResult import io.github.wulkanowy.services.sync.notifications.NewGradeNotification -import io.github.wulkanowy.utils.waitForResult import kotlinx.coroutines.flow.first import javax.inject.Inject class GradeWork @Inject constructor( private val gradeRepository: GradeRepository, - private val preferencesRepository: PreferencesRepository, private val newGradeNotification: NewGradeNotification, ) : Work { - override suspend fun doWork(student: Student, semester: Semester) { + override suspend fun doWork(student: Student, semester: Semester, notify: Boolean) { gradeRepository.getGrades( student = student, semester = semester, forceRefresh = true, - notify = preferencesRepository.isNotificationsEnable + notify = notify, ).waitForResult() gradeRepository.getGradesFromDatabase(semester).first() 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 2a5d2d7c0..4cfe27d0d 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 @@ -3,28 +3,26 @@ 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.HomeworkRepository -import io.github.wulkanowy.data.repositories.PreferencesRepository +import io.github.wulkanowy.data.waitForResult import io.github.wulkanowy.services.sync.notifications.NewHomeworkNotification 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 HomeworkWork @Inject constructor( private val homeworkRepository: HomeworkRepository, - private val preferencesRepository: PreferencesRepository, private val newHomeworkNotification: NewHomeworkNotification, ) : Work { - override suspend fun doWork(student: Student, semester: Semester) { + override suspend fun doWork(student: Student, semester: Semester, notify: Boolean) { homeworkRepository.getHomework( student = student, semester = semester, start = now().nextOrSameSchoolDay, end = now().nextOrSameSchoolDay, forceRefresh = true, - notify = preferencesRepository.isNotificationsEnable + notify = notify, ).waitForResult() homeworkRepository.getHomeworkFromDatabase(semester, now(), now().plusDays(7)).first() diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/works/LuckyNumberWork.kt b/app/src/main/java/io/github/wulkanowy/services/sync/works/LuckyNumberWork.kt index 348f92142..668b1b6b8 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/works/LuckyNumberWork.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/works/LuckyNumberWork.kt @@ -3,22 +3,20 @@ 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.LuckyNumberRepository -import io.github.wulkanowy.data.repositories.PreferencesRepository +import io.github.wulkanowy.data.waitForResult import io.github.wulkanowy.services.sync.notifications.NewLuckyNumberNotification -import io.github.wulkanowy.utils.waitForResult import javax.inject.Inject class LuckyNumberWork @Inject constructor( private val luckyNumberRepository: LuckyNumberRepository, - private val preferencesRepository: PreferencesRepository, private val newLuckyNumberNotification: NewLuckyNumberNotification, ) : Work { - override suspend fun doWork(student: Student, semester: Semester) { + override suspend fun doWork(student: Student, semester: Semester, notify: Boolean) { luckyNumberRepository.getLuckyNumber( student = student, forceRefresh = true, - notify = preferencesRepository.isNotificationsEnable + notify = notify, ).waitForResult() luckyNumberRepository.getNotNotifiedLuckyNumber(student)?.let { diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/works/MessageWork.kt b/app/src/main/java/io/github/wulkanowy/services/sync/works/MessageWork.kt index b5624a76d..180568267 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/works/MessageWork.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/works/MessageWork.kt @@ -3,29 +3,30 @@ 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.enums.MessageFolder.RECEIVED +import io.github.wulkanowy.data.repositories.MailboxRepository import io.github.wulkanowy.data.repositories.MessageRepository -import io.github.wulkanowy.data.repositories.PreferencesRepository +import io.github.wulkanowy.data.waitForResult import io.github.wulkanowy.services.sync.notifications.NewMessageNotification -import io.github.wulkanowy.utils.waitForResult import kotlinx.coroutines.flow.first import javax.inject.Inject class MessageWork @Inject constructor( private val messageRepository: MessageRepository, - private val preferencesRepository: PreferencesRepository, + private val mailboxRepository: MailboxRepository, private val newMessageNotification: NewMessageNotification, ) : Work { - override suspend fun doWork(student: Student, semester: Semester) { + override suspend fun doWork(student: Student, semester: Semester, notify: Boolean) { + val mailbox = mailboxRepository.getMailbox(student) messageRepository.getMessages( student = student, - semester = semester, + mailbox = mailbox, folder = RECEIVED, forceRefresh = true, - notify = preferencesRepository.isNotificationsEnable + notify = notify ).waitForResult() - messageRepository.getMessagesFromDatabase(student).first() + messageRepository.getMessagesFromDatabase(mailbox).first() .filter { !it.isNotified && it.unread }.let { if (it.isNotEmpty()) newMessageNotification.notify(it, student) messageRepository.updateMessages(it.onEach { message -> message.isNotified = true }) diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/works/NoteWork.kt b/app/src/main/java/io/github/wulkanowy/services/sync/works/NoteWork.kt index 6f18eddf1..df6e2b06b 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/works/NoteWork.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/works/NoteWork.kt @@ -3,24 +3,22 @@ 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.NoteRepository -import io.github.wulkanowy.data.repositories.PreferencesRepository +import io.github.wulkanowy.data.waitForResult import io.github.wulkanowy.services.sync.notifications.NewNoteNotification -import io.github.wulkanowy.utils.waitForResult import kotlinx.coroutines.flow.first import javax.inject.Inject class NoteWork @Inject constructor( private val noteRepository: NoteRepository, - private val preferencesRepository: PreferencesRepository, private val newNoteNotification: NewNoteNotification, ) : Work { - override suspend fun doWork(student: Student, semester: Semester) { + override suspend fun doWork(student: Student, semester: Semester, notify: Boolean) { noteRepository.getNotes( student = student, semester = semester, forceRefresh = true, - notify = preferencesRepository.isNotificationsEnable + notify = notify, ).waitForResult() noteRepository.getNotesFromDatabase(student).first() diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/works/RecipientWork.kt b/app/src/main/java/io/github/wulkanowy/services/sync/works/RecipientWork.kt index 34ab3db04..b1322ada3 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/works/RecipientWork.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/works/RecipientWork.kt @@ -1,23 +1,22 @@ package io.github.wulkanowy.services.sync.works +import io.github.wulkanowy.data.db.entities.MailboxType import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student +import io.github.wulkanowy.data.repositories.MailboxRepository import io.github.wulkanowy.data.repositories.RecipientRepository -import io.github.wulkanowy.data.repositories.ReportingUnitRepository import javax.inject.Inject class RecipientWork @Inject constructor( - private val reportingUnitRepository: ReportingUnitRepository, + private val mailboxRepository: MailboxRepository, private val recipientRepository: RecipientRepository ) : Work { - override suspend fun doWork(student: Student, semester: Semester) { - reportingUnitRepository.refreshReportingUnits(student) + override suspend fun doWork(student: Student, semester: Semester, notify: Boolean) { + mailboxRepository.refreshMailboxes(student) - reportingUnitRepository.getReportingUnits(student).let { units -> - units.map { - recipientRepository.refreshRecipients(student, it, 2) - } - } + val mailbox = mailboxRepository.getMailbox(student) + + recipientRepository.refreshRecipients(student, mailbox, MailboxType.EMPLOYEE) } } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/works/SchoolAnnouncementWork.kt b/app/src/main/java/io/github/wulkanowy/services/sync/works/SchoolAnnouncementWork.kt index 268992f46..1aedc8399 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/works/SchoolAnnouncementWork.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/works/SchoolAnnouncementWork.kt @@ -2,30 +2,32 @@ 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.SchoolAnnouncementRepository +import io.github.wulkanowy.data.waitForResult import io.github.wulkanowy.services.sync.notifications.NewSchoolAnnouncementNotification -import io.github.wulkanowy.utils.waitForResult import kotlinx.coroutines.flow.first +import java.time.LocalDate import javax.inject.Inject class SchoolAnnouncementWork @Inject constructor( private val schoolAnnouncementRepository: SchoolAnnouncementRepository, - private val preferencesRepository: PreferencesRepository, private val newSchoolAnnouncementNotification: NewSchoolAnnouncementNotification, ) : Work { - override suspend fun doWork(student: Student, semester: Semester) { + override suspend fun doWork(student: Student, semester: Semester, notify: Boolean) { schoolAnnouncementRepository.getSchoolAnnouncements( student = student, forceRefresh = true, - notify = preferencesRepository.isNotificationsEnable + notify = notify, ).waitForResult() - - schoolAnnouncementRepository.getSchoolAnnouncementFromDatabase(student).first() - .filter { !it.isNotified }.let { - if (it.isNotEmpty()) newSchoolAnnouncementNotification.notify(it, student) + schoolAnnouncementRepository.getSchoolAnnouncementFromDatabase(student) + .first() + .filter { !it.isNotified && it.date >= LocalDate.now() } + .let { + if (it.isNotEmpty()) { + newSchoolAnnouncementNotification.notify(it, student) + } schoolAnnouncementRepository.updateSchoolAnnouncement(it.onEach { schoolAnnouncement -> schoolAnnouncement.isNotified = true diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/works/TeacherWork.kt b/app/src/main/java/io/github/wulkanowy/services/sync/works/TeacherWork.kt index 7c614c6c5..e7c72bf00 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/works/TeacherWork.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/works/TeacherWork.kt @@ -3,12 +3,13 @@ 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.TeacherRepository -import io.github.wulkanowy.utils.waitForResult +import io.github.wulkanowy.data.waitForResult + import javax.inject.Inject class TeacherWork @Inject constructor(private val teacherRepository: TeacherRepository) : Work { - override suspend fun doWork(student: Student, semester: Semester) { + override suspend fun doWork(student: Student, semester: Semester, notify: Boolean) { teacherRepository.getTeachers(student, semester, true).waitForResult() } } 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 fcc330638..29b1f13c7 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,11 +2,10 @@ 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.data.waitForResult 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 @@ -14,17 +13,16 @@ import javax.inject.Inject class TimetableWork @Inject constructor( private val timetableRepository: TimetableRepository, private val changeTimetableNotification: ChangeTimetableNotification, - private val preferencesRepository: PreferencesRepository ) : Work { - override suspend fun doWork(student: Student, semester: Semester) { + override suspend fun doWork(student: Student, semester: Semester, notify: Boolean) { timetableRepository.getTimetable( student = student, semester = semester, start = now().nextOrSameSchoolDay, end = now().nextOrSameSchoolDay, forceRefresh = true, - notify = preferencesRepository.isNotificationsEnable + notify = notify, ) .waitForResult() diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/works/Work.kt b/app/src/main/java/io/github/wulkanowy/services/sync/works/Work.kt index c41f41ce2..1c0214cdd 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/works/Work.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/works/Work.kt @@ -5,5 +5,5 @@ import io.github.wulkanowy.data.db.entities.Student interface Work { - suspend fun doWork(student: Student, semester: Semester) + suspend fun doWork(student: Student, semester: Semester, notify: Boolean) } 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 5cd5d0109..15c069f54 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 @@ -1,17 +1,10 @@ package io.github.wulkanowy.ui.base -import io.github.wulkanowy.data.Status import io.github.wulkanowy.data.repositories.StudentRepository -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.* import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach import timber.log.Timber open class BasePresenter( @@ -37,28 +30,28 @@ open class BasePresenter( } fun onExpiredLoginSelected() { - flowWithResource { - val student = studentRepository.getCurrentStudent(false) - studentRepository.logoutStudent(student) + Timber.i("Attempt to switch the student after the session expires") - val students = studentRepository.getSavedStudents(false) - if (students.isNotEmpty()) { - Timber.i("Switching current student") - studentRepository.switchStudent(students[0]) + presenterScope.launch { + runCatching { + val student = studentRepository.getCurrentStudent(false) + studentRepository.logoutStudent(student) + + val students = studentRepository.getSavedStudents(false) + if (students.isNotEmpty()) { + Timber.i("Switching current student") + studentRepository.switchStudent(students[0]) + } } - }.onEach { - when (it.status) { - Status.LOADING -> Timber.i("Attempt to switch the student after the session expires") - Status.SUCCESS -> { + .onFailure { + Timber.i("Switch student result: An exception occurred") + errorHandler.dispatch(it) + } + .onSuccess { Timber.i("Switch student result: Open login view") view?.openClearLoginView() } - Status.ERROR -> { - Timber.i("Switch student result: An exception occurred") - errorHandler.dispatch(it.error!!) - } - } - }.launch("expired") + } } fun Flow.launch(individualJobTag: String = "load"): Job { 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 4c279d816..48c003b7e 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 @@ -1,102 +1,84 @@ package io.github.wulkanowy.ui.base +import android.app.Dialog import android.content.ClipData import android.content.ClipboardManager import android.os.Bundle import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.HorizontalScrollView import android.widget.Toast import android.widget.Toast.LENGTH_LONG import androidx.appcompat.app.AlertDialog import androidx.core.content.getSystemService +import androidx.core.os.bundleOf import androidx.core.view.isGone +import androidx.fragment.app.DialogFragment +import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.databinding.DialogErrorBinding -import io.github.wulkanowy.sdk.exception.FeatureNotAvailableException -import io.github.wulkanowy.sdk.scrapper.exception.FeatureDisabledException -import io.github.wulkanowy.sdk.scrapper.exception.ServiceUnavailableException -import io.github.wulkanowy.utils.AppInfo -import io.github.wulkanowy.utils.getString -import io.github.wulkanowy.utils.openAppInMarket -import io.github.wulkanowy.utils.openEmailClient -import io.github.wulkanowy.utils.openInternetBrowser -import okhttp3.internal.http2.StreamResetException -import java.io.InterruptedIOException -import java.net.ConnectException -import java.net.SocketTimeoutException -import java.net.UnknownHostException +import io.github.wulkanowy.utils.* import javax.inject.Inject @AndroidEntryPoint -class ErrorDialog : BaseDialogFragment() { - - private lateinit var error: Throwable +class ErrorDialog : DialogFragment() { @Inject lateinit var appInfo: AppInfo companion object { - private const val ARGUMENT_KEY = "Data" + private const val ARGUMENT_KEY = "error" fun newInstance(error: Throwable) = ErrorDialog().apply { - arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, error) } + arguments = bundleOf(ARGUMENT_KEY to error) } } - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setStyle(STYLE_NO_TITLE, 0) - arguments?.run { - error = getSerializable(ARGUMENT_KEY) as Throwable + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val error = requireArguments().getSerializable(ARGUMENT_KEY) as Throwable + + val binding = DialogErrorBinding.inflate(LayoutInflater.from(context)) + binding.bindErrorDetails(error) + + return getAlertDialog(binding, error).apply { + enableReportButtonIfErrorIsReportable(error) } } - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ) = DialogErrorBinding.inflate(inflater).apply { binding = this }.root - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - val errorStacktrace = error.stackTraceToString() - - with(binding) { - errorDialogContent.text = errorStacktrace.replace(": ${error.localizedMessage}", "") - with(errorDialogHorizontalScroll) { - post { fullScroll(HorizontalScrollView.FOCUS_LEFT) } - } - errorDialogCopy.setOnClickListener { - 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 { + private fun getAlertDialog(binding: DialogErrorBinding, error: Throwable): AlertDialog { + return MaterialAlertDialogBuilder(requireContext()).apply { + val errorStacktrace = error.stackTraceToString() + setTitle(R.string.all_details) + setView(binding.root) + setNeutralButton(R.string.about_feedback) { _, _ -> openConfirmDialog { openEmailClient(errorStacktrace) } } - errorDialogHumanizedMessage.text = resources.getString(error) + setNegativeButton(android.R.string.cancel) { _, _ -> } + setPositiveButton(android.R.string.copy) { _, _ -> copyErrorToClipboard(errorStacktrace) } + }.create() + } + + private fun DialogErrorBinding.bindErrorDetails(error: Throwable) { + return with(this) { + errorDialogHumanizedMessage.text = resources.getErrorString(error) errorDialogErrorMessage.text = error.localizedMessage errorDialogErrorMessage.isGone = error.localizedMessage.isNullOrBlank() - errorDialogReport.isEnabled = when (error) { - is UnknownHostException, - is InterruptedIOException, - is ConnectException, - is StreamResetException, - is SocketTimeoutException, - is ServiceUnavailableException, - is FeatureDisabledException, - is FeatureNotAvailableException -> false - else -> true - } + errorDialogContent.text = error.stackTraceToString() + .replace(": ${error.localizedMessage}", "") } } + private fun AlertDialog.enableReportButtonIfErrorIsReportable(error: Throwable) { + setOnShowListener { + getButton(AlertDialog.BUTTON_NEUTRAL).isEnabled = error.isShouldBeReported() + } + } + + private fun copyErrorToClipboard(errorStacktrace: String) { + val clip = ClipData.newPlainText("Error details", errorStacktrace) + requireActivity().getSystemService()?.setPrimaryClip(clip) + Toast.makeText(requireContext(), R.string.all_copied, LENGTH_LONG).show() + } + private fun openConfirmDialog(callback: () -> Unit) { AlertDialog.Builder(requireContext()) .setTitle(R.string.dialog_error_check_update) @@ -127,4 +109,8 @@ class ErrorDialog : BaseDialogFragment() { } ) } + + private fun showMessage(text: String) { + Toast.makeText(requireContext(), text, LENGTH_LONG).show() + } } 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 fbc994e2c..afe200e9a 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 @@ -5,7 +5,7 @@ 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 -import io.github.wulkanowy.utils.getString +import io.github.wulkanowy.utils.getErrorString import io.github.wulkanowy.utils.security.ScramblerException import timber.log.Timber import javax.inject.Inject @@ -26,7 +26,7 @@ open class ErrorHandler @Inject constructor(@ApplicationContext protected val co } protected open fun proceed(error: Throwable) { - showErrorMessage(context.resources.getString(error), error) + showErrorMessage(context.resources.getErrorString(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/modules/Destination.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/Destination.kt index 43d4b5f9f..561419a05 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/Destination.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/Destination.kt @@ -1,6 +1,7 @@ package io.github.wulkanowy.ui.modules import androidx.fragment.app.Fragment +import io.github.wulkanowy.data.serializers.LocalDateSerializer import io.github.wulkanowy.ui.modules.attendance.AttendanceFragment import io.github.wulkanowy.ui.modules.conference.ConferenceFragment import io.github.wulkanowy.ui.modules.dashboard.DashboardFragment @@ -14,18 +15,19 @@ 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 kotlinx.serialization.Serializable import java.time.LocalDate -sealed interface Destination : Serializable { +@Serializable +sealed class Destination { /* 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 + abstract val destinationType: Type - val fragment: Fragment + abstract val destinationFragment: Fragment enum class Type(val defaultDestination: Destination) { DASHBOARD(Dashboard), @@ -43,94 +45,84 @@ sealed interface Destination : Serializable { MESSAGE(Message); } - object Dashboard : Destination { - - override val type get() = Type.DASHBOARD - - override val fragment get() = DashboardFragment.newInstance() + @Serializable + object Dashboard : Destination() { + override val destinationType get() = Type.DASHBOARD + override val destinationFragment get() = DashboardFragment.newInstance() } - object Grade : Destination { - - override val type get() = Type.GRADE - - override val fragment get() = GradeFragment.newInstance() + @Serializable + object Grade : Destination() { + override val destinationType get() = Type.GRADE + override val destinationFragment get() = GradeFragment.newInstance() } - object Attendance : Destination { - - override val type get() = Type.ATTENDANCE - - override val fragment get() = AttendanceFragment.newInstance() + @Serializable + object Attendance : Destination() { + override val destinationType get() = Type.ATTENDANCE + override val destinationFragment get() = AttendanceFragment.newInstance() } - object Exam : Destination { - - override val type get() = Type.EXAM - - override val fragment get() = ExamFragment.newInstance() + @Serializable + object Exam : Destination() { + override val destinationType get() = Type.EXAM + override val destinationFragment get() = ExamFragment.newInstance() } - data class Timetable(val date: LocalDate? = null) : Destination { - - override val type get() = Type.TIMETABLE - - override val fragment get() = TimetableFragment.newInstance(date) + @Serializable + data class Timetable( + @Serializable(with = LocalDateSerializer::class) + private val date: LocalDate? = null + ) : Destination() { + override val destinationType get() = Type.TIMETABLE + override val destinationFragment get() = TimetableFragment.newInstance(date) } - object Homework : Destination { - - override val type get() = Type.HOMEWORK - - override val fragment get() = HomeworkFragment.newInstance() + @Serializable + object Homework : Destination() { + override val destinationType get() = Type.HOMEWORK + override val destinationFragment get() = HomeworkFragment.newInstance() } - object Note : Destination { - - override val type get() = Type.NOTE - - override val fragment get() = NoteFragment.newInstance() + @Serializable + object Note : Destination() { + override val destinationType get() = Type.NOTE + override val destinationFragment get() = NoteFragment.newInstance() } - object Conference : Destination { - - override val type get() = Type.CONFERENCE - - override val fragment get() = ConferenceFragment.newInstance() + @Serializable + object Conference : Destination() { + override val destinationType get() = Type.CONFERENCE + override val destinationFragment get() = ConferenceFragment.newInstance() } - object SchoolAnnouncement : Destination { - - override val type get() = Type.SCHOOL_ANNOUNCEMENT - - override val fragment get() = SchoolAnnouncementFragment.newInstance() + @Serializable + object SchoolAnnouncement : Destination() { + override val destinationType get() = Type.SCHOOL_ANNOUNCEMENT + override val destinationFragment get() = SchoolAnnouncementFragment.newInstance() } - object School : Destination { - - override val type get() = Type.SCHOOL - - override val fragment get() = SchoolFragment.newInstance() + @Serializable + object School : Destination() { + override val destinationType get() = Type.SCHOOL + override val destinationFragment get() = SchoolFragment.newInstance() } - object LuckyNumber : Destination { - - override val type get() = Type.LUCKY_NUMBER - - override val fragment get() = LuckyNumberFragment.newInstance() + @Serializable + object LuckyNumber : Destination() { + override val destinationType get() = Type.LUCKY_NUMBER + override val destinationFragment get() = LuckyNumberFragment.newInstance() } - object More : Destination { - - override val type get() = Type.MORE - - override val fragment get() = MoreFragment.newInstance() + @Serializable + object More : Destination() { + override val destinationType get() = Type.MORE + override val destinationFragment get() = MoreFragment.newInstance() } - object Message : Destination { - - override val type get() = Type.MESSAGE - - override val fragment get() = MessageFragment.newInstance() + @Serializable + object Message : Destination() { + override val destinationType get() = Type.MESSAGE + override val destinationFragment get() = MessageFragment.newInstance() } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/about/AboutFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/about/AboutFragment.kt index 1bf5c7ad9..701656b55 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/about/AboutFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/about/AboutFragment.kt @@ -13,13 +13,8 @@ import io.github.wulkanowy.ui.modules.about.license.LicenseFragment import io.github.wulkanowy.ui.modules.debug.DebugFragment import io.github.wulkanowy.ui.modules.main.MainActivity import io.github.wulkanowy.ui.modules.main.MainView -import io.github.wulkanowy.utils.AppInfo -import io.github.wulkanowy.utils.getCompatDrawable -import io.github.wulkanowy.utils.openAppInMarket -import io.github.wulkanowy.utils.openEmailClient -import io.github.wulkanowy.utils.openInternetBrowser -import io.github.wulkanowy.utils.toFormattedString -import io.github.wulkanowy.utils.toLocalDateTime +import io.github.wulkanowy.utils.* +import java.time.Instant import javax.inject.Inject @AndroidEntryPoint @@ -38,7 +33,7 @@ class AboutFragment : BaseFragment(R.layout.fragment_about override val versionRes: Triple? get() = context?.run { val buildTimestamp = - appInfo.buildTimestamp.toLocalDateTime().toFormattedString("yyyy-MM-dd") + Instant.ofEpochMilli(appInfo.buildTimestamp).toFormattedString("yyyy-MM-dd") val versionSignature = "${appInfo.versionName}-${appInfo.buildFlavor} (${appInfo.versionCode}), $buildTimestamp" Triple( diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/about/contributor/ContributorPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/about/contributor/ContributorPresenter.kt index ef4b540e6..126bb2b48 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/about/contributor/ContributorPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/about/contributor/ContributorPresenter.kt @@ -1,13 +1,11 @@ package io.github.wulkanowy.ui.modules.about.contributor -import io.github.wulkanowy.data.Status +import io.github.wulkanowy.data.* import io.github.wulkanowy.data.pojos.Contributor import io.github.wulkanowy.data.repositories.AppCreatorRepository 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 javax.inject.Inject class ContributorPresenter @Inject constructor( @@ -31,15 +29,11 @@ class ContributorPresenter @Inject constructor( } private fun loadData() { - flowWithResource { appCreatorRepository.getAppCreators() }.onEach { - when (it.status) { - Status.LOADING -> view?.showProgress(true) - Status.SUCCESS -> view?.run { - showProgress(false) - updateData(it.data!!) - } - Status.ERROR -> errorHandler.dispatch(it.error!!) - } - }.launch() + resourceFlow { appCreatorRepository.getAppCreators() } + .onResourceLoading { view?.showProgress(true) } + .onResourceSuccess { view?.updateData(it) } + .onResourceNotLoading { view?.showProgress(false) } + .onResourceError { errorHandler.dispatch(it) } + .launch() } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/about/license/LicenseAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/about/license/LicenseAdapter.kt index 6ae06bbe7..adf4ca741 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/about/license/LicenseAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/about/license/LicenseAdapter.kt @@ -23,8 +23,9 @@ class LicenseAdapter @Inject constructor() : RecyclerView.Adapter(R.layout.fragment_l override val titleStringId get() = R.string.license_title - override val appLibraries by lazy { Libs(requireContext()).libraries } + override val appLibraries by lazy { + Libs.Builder().withContext(requireContext()).build().libraries + } companion object { fun newInstance() = LicenseFragment() 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 5368cc19d..ddcd5918f 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 @@ -1,16 +1,12 @@ package io.github.wulkanowy.ui.modules.about.license import com.mikepenz.aboutlibraries.entity.Library -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.DispatchersProvider -import io.github.wulkanowy.utils.afterLoading -import io.github.wulkanowy.utils.flowWithResource -import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import timber.log.Timber import javax.inject.Inject class LicensePresenter @Inject constructor( @@ -26,22 +22,20 @@ class LicensePresenter @Inject constructor( } fun onItemSelected(library: Library) { - view?.run { library.licenses?.firstOrNull()?.licenseDescription?.let { openLicense(it) } } + view?.run { library.licenses.firstOrNull()?.licenseContent?.let { openLicense(it) } } } private fun loadData() { - flowWithResource { - withContext(dispatchers.io) { - view?.appLibraries.orEmpty() + presenterScope.launch { + runCatching { + withContext(dispatchers.io) { + view?.appLibraries.orEmpty() + } } - }.onEach { - when (it.status) { - Status.LOADING -> Timber.d("License data load started") - Status.SUCCESS -> view?.updateData(it.data!!) - Status.ERROR -> errorHandler.dispatch(it.error!!) - } - }.afterLoading { + .onFailure { errorHandler.dispatch(it) } + .onSuccess { view?.updateData(it) } + view?.showProgress(false) - }.launch() + } } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/account/AccountPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/account/AccountPresenter.kt index 7fe77ca7a..77c1ffe64 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/account/AccountPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/account/AccountPresenter.kt @@ -1,12 +1,13 @@ package io.github.wulkanowy.ui.modules.account -import io.github.wulkanowy.data.Status import io.github.wulkanowy.data.db.entities.StudentWithSemesters +import io.github.wulkanowy.data.logResourceStatus +import io.github.wulkanowy.data.onResourceError +import io.github.wulkanowy.data.onResourceSuccess import io.github.wulkanowy.data.repositories.StudentRepository +import io.github.wulkanowy.data.resourceFlow import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.ErrorHandler -import io.github.wulkanowy.utils.flowWithResource -import kotlinx.coroutines.flow.onEach import timber.log.Timber import javax.inject.Inject @@ -32,20 +33,10 @@ class AccountPresenter @Inject constructor( } private fun loadData() { - flowWithResource { studentRepository.getSavedStudents(false) } - .onEach { - when (it.status) { - Status.LOADING -> Timber.i("Loading account data started") - Status.SUCCESS -> { - Timber.i("Loading account result: Success") - view?.updateData(createAccountItems(it.data!!)) - } - Status.ERROR -> { - Timber.i("Loading account result: An exception occurred") - errorHandler.dispatch(it.error!!) - } - } - } + resourceFlow { studentRepository.getSavedStudents(false) } + .logResourceStatus("load account data") + .onResourceSuccess { view?.updateData(createAccountItems(it)) } + .onResourceError(errorHandler::dispatch) .launch("load") } 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 1f44cbbc3..5d68ff2e1 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 @@ -1,7 +1,6 @@ package io.github.wulkanowy.ui.modules.account.accountdetails -import io.github.wulkanowy.data.Resource -import io.github.wulkanowy.data.Status +import io.github.wulkanowy.data.* import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.data.repositories.StudentRepository @@ -9,10 +8,6 @@ import io.github.wulkanowy.services.sync.SyncManager import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.ui.modules.studentinfo.StudentInfoView -import io.github.wulkanowy.utils.afterLoading -import io.github.wulkanowy.utils.flowWithResource -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach import timber.log.Timber import javax.inject.Inject @@ -51,40 +46,25 @@ class AccountDetailsPresenter @Inject constructor( } private fun loadData() { - flowWithResource { studentRepository.getSavedStudents() } - .map { studentWithSemesters -> - Resource( - data = studentWithSemesters.data?.single { it.student.id == studentId }, - status = studentWithSemesters.status, - error = studentWithSemesters.error - ) - } - .onEach { - when (it.status) { - Status.LOADING -> { - view?.run { - showProgress(true) - showContent(false) - } - Timber.i("Loading account details view started") - } - Status.SUCCESS -> { - Timber.i("Loading account details view result: Success") - studentWithSemesters = it.data - view?.run { - showAccountData(studentWithSemesters!!.student) - enableSelectStudentButton(!studentWithSemesters!!.student.isCurrent) - showContent(true) - showErrorView(false) - } - } - Status.ERROR -> { - Timber.i("Loading account details view result: An exception occurred") - errorHandler.dispatch(it.error!!) - } + resourceFlow { studentRepository.getSavedStudentById(studentId ?: -1) } + .logResourceStatus("loading account details view") + .onResourceLoading { + view?.run { + showProgress(true) + showContent(false) } } - .afterLoading { view?.showProgress(false) } + .onResourceSuccess { + studentWithSemesters = it + view?.run { + showAccountData(studentWithSemesters!!.student) + enableSelectStudentButton(!studentWithSemesters!!.student.isCurrent) + showContent(true) + showErrorView(false) + } + } + .onResourceNotLoading { view?.showProgress(false) } + .onResourceError(errorHandler::dispatch) .launch() } @@ -105,22 +85,12 @@ class AccountDetailsPresenter @Inject constructor( Timber.i("Select student ${studentWithSemesters!!.student.id}") - flowWithResource { studentRepository.switchStudent(studentWithSemesters!!) } - .onEach { - when (it.status) { - Status.LOADING -> Timber.i("Attempt to change a student") - Status.SUCCESS -> { - Timber.i("Change a student result: Success") - view?.recreateMainView() - } - Status.ERROR -> { - Timber.i("Change a student result: An exception occurred") - errorHandler.dispatch(it.error!!) - } - } - }.afterLoading { - view?.popViewToMain() - }.launch("switch") + resourceFlow { studentRepository.switchStudent(studentWithSemesters!!) } + .logResourceStatus("change student") + .onResourceSuccess { view?.recreateMainView() } + .onResourceNotLoading { view?.popViewToMain() } + .onResourceError(errorHandler::dispatch) + .launch("switch") } fun onRemoveSelected() { @@ -131,7 +101,7 @@ class AccountDetailsPresenter @Inject constructor( fun onLogoutConfirm() { if (studentWithSemesters == null) return - flowWithResource { + resourceFlow { val studentToLogout = studentWithSemesters!!.student studentRepository.logoutStudent(studentToLogout) @@ -141,13 +111,13 @@ class AccountDetailsPresenter @Inject constructor( studentRepository.switchStudent(students[0]) } - return@flowWithResource students - }.onEach { - when (it.status) { - Status.LOADING -> Timber.i("Attempt to logout user") - Status.SUCCESS -> view?.run { + students + } + .logResourceStatus("logout user") + .onResourceSuccess { + view?.run { when { - it.data!!.isEmpty() -> { + it.isEmpty() -> { Timber.i("Logout result: Open login view") syncManager.stopSyncWorker() openClearLoginView() @@ -162,18 +132,16 @@ class AccountDetailsPresenter @Inject constructor( } } } - Status.ERROR -> { - Timber.i("Logout result: An exception occurred") - errorHandler.dispatch(it.error!!) + } + .onResourceNotLoading { + if (studentWithSemesters?.student?.isCurrent == true) { + view?.popViewToMain() + } else { + view?.popViewToAccounts() } } - }.afterLoading { - if (studentWithSemesters?.student?.isCurrent == true) { - view?.popViewToMain() - } else { - view?.popViewToAccounts() - } - }.launch("logout") + .onResourceError(errorHandler::dispatch) + .launch("logout") } private fun showErrorViewOnError(message: String, error: Throwable) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountedit/AccountEditPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountedit/AccountEditPresenter.kt index 67ecdb5fc..c401158ea 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountedit/AccountEditPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountedit/AccountEditPresenter.kt @@ -1,15 +1,12 @@ package io.github.wulkanowy.ui.modules.account.accountedit -import io.github.wulkanowy.data.Status +import io.github.wulkanowy.data.* import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.StudentNickAndAvatar 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.AppInfo -import io.github.wulkanowy.utils.afterLoading -import io.github.wulkanowy.utils.flowWithResource -import kotlinx.coroutines.flow.onEach import timber.log.Timber import javax.inject.Inject @@ -38,43 +35,26 @@ class AccountEditPresenter @Inject constructor( } private fun loadData() { - flowWithResource { - studentRepository.getStudentById(student.id, false).avatarColor - }.onEach { resource -> - when (resource.status) { - Status.LOADING -> Timber.i("Attempt to load student") - Status.SUCCESS -> { - view?.updateSelectedColorData(resource.data?.toInt()!!) - Timber.i("Attempt to load student: Success") - } - Status.ERROR -> { - Timber.i("Attempt to load student: An exception occurred") - errorHandler.dispatch(resource.error!!) - } - } - }.launch("load_data") + resourceFlow { studentRepository.getStudentById(student.id, false).avatarColor } + .logResourceStatus("load student") + .onResourceSuccess { view?.updateSelectedColorData(it.toInt()) } + .onResourceError(errorHandler::dispatch) + .launch("load_data") } fun changeStudentNickAndAvatar(nick: String, avatarColor: Int) { - flowWithResource { - val studentNick = - StudentNickAndAvatar(nick = nick.trim(), avatarColor = avatarColor.toLong()) - .apply { id = student.id } + resourceFlow { + val studentNick = StudentNickAndAvatar( + nick = nick.trim(), + avatarColor = avatarColor.toLong() + ).apply { id = student.id } + studentRepository.updateStudentNickAndAvatar(studentNick) - }.onEach { - when (it.status) { - Status.LOADING -> Timber.i("Attempt to change a student nick and avatar") - Status.SUCCESS -> { - Timber.i("Change a student nick and avatar result: Success") - view?.recreateMainView() - } - Status.ERROR -> { - Timber.i("Change a student nick and avatar result: An exception occurred") - errorHandler.dispatch(it.error!!) - } - } } - .afterLoading { view?.popView() } + .logResourceStatus("change student nick and avatar") + .onResourceSuccess { view?.recreateMainView() } + .onResourceNotLoading { view?.popView() } + .onResourceError(errorHandler::dispatch) .launch("update_student") } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountquick/AccountQuickPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountquick/AccountQuickPresenter.kt index 39d8fce24..32c07f80a 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountquick/AccountQuickPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountquick/AccountQuickPresenter.kt @@ -1,14 +1,11 @@ package io.github.wulkanowy.ui.modules.account.accountquick -import io.github.wulkanowy.data.Status +import io.github.wulkanowy.data.* import io.github.wulkanowy.data.db.entities.StudentWithSemesters 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.account.AccountItem -import io.github.wulkanowy.utils.afterLoading -import io.github.wulkanowy.utils.flowWithResource -import kotlinx.coroutines.flow.onEach import timber.log.Timber import javax.inject.Inject @@ -43,21 +40,11 @@ class AccountQuickPresenter @Inject constructor( return } - flowWithResource { studentRepository.switchStudent(studentWithSemesters) } - .onEach { - when (it.status) { - Status.LOADING -> Timber.i("Attempt to change a student") - Status.SUCCESS -> { - Timber.i("Change a student result: Success") - view?.recreateMainView() - } - Status.ERROR -> { - Timber.i("Change a student result: An exception occurred") - errorHandler.dispatch(it.error!!) - } - } - } - .afterLoading { view?.popView() } + resourceFlow { studentRepository.switchStudent(studentWithSemesters) } + .logResourceStatus("change student") + .onResourceSuccess { view?.recreateMainView() } + .onResourceNotLoading { view?.popView() } + .onResourceError(errorHandler::dispatch) .launch("switch") } 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 5d5ed504c..39f376f65 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 @@ -35,9 +35,11 @@ class AttendanceAdapter @Inject constructor() : with(holder.binding) { attendanceItemNumber.text = item.number.toString() - attendanceItemSubject.text = item.subject + attendanceItemSubject.text = item.subject.ifBlank { + root.context.getString(R.string.all_no_data) + } attendanceItemDescription.setText(item.descriptionRes) - attendanceItemAlert.visibility = item.run { if (absence && !excused) View.VISIBLE else View.INVISIBLE } + attendanceItemAlert.isVisible = item.let { it.absence && !it.excused } attendanceItemNumber.visibility = View.GONE attendanceItemExcuseInfo.visibility = View.GONE attendanceItemExcuseCheckbox.visibility = View.GONE @@ -46,7 +48,7 @@ class AttendanceAdapter @Inject constructor() : onExcuseCheckboxSelect(item, checked) } - when (item.excuseStatus?.let { SentExcuseStatus.valueOf(it)}) { + 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/AttendanceFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceFragment.kt index bd9a66926..6354b5e04 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 @@ -2,20 +2,12 @@ package io.github.wulkanowy.ui.modules.attendance import android.content.DialogInterface.BUTTON_POSITIVE import android.os.Bundle -import android.view.LayoutInflater -import android.view.Menu -import android.view.MenuInflater -import android.view.MenuItem -import android.view.View -import android.view.View.GONE -import android.view.View.INVISIBLE -import android.view.View.VISIBLE +import android.view.* +import android.view.View.* 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 import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Attendance @@ -27,12 +19,10 @@ 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.ui.widgets.DividerItemDecoration -import io.github.wulkanowy.utils.SchoolDaysValidator import io.github.wulkanowy.utils.dpToPx +import io.github.wulkanowy.utils.firstSchoolDayInSchoolYear import io.github.wulkanowy.utils.getThemeAttrColor -import io.github.wulkanowy.utils.schoolYearStart -import io.github.wulkanowy.utils.toLocalDateTime -import io.github.wulkanowy.utils.toTimestamp +import io.github.wulkanowy.utils.openMaterialDatePicker import java.time.LocalDate import javax.inject.Inject @@ -72,7 +62,7 @@ class AttendanceFragment : BaseFragment(R.layout.frag private val actionModeCallback = object : ActionMode.Callback { override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { val inflater = mode.menuInflater - inflater.inflate(R.menu.context_menu_excuse, menu) + inflater.inflate(R.menu.context_menu_attendance, menu) return true } @@ -224,29 +214,15 @@ class AttendanceFragment : BaseFragment(R.layout.frag (activity as? MainActivity)?.showDialogFragment(AttendanceDialog.newInstance(lesson)) } - override fun showDatePickerDialog(currentDate: LocalDate) { - val baseDate = currentDate.schoolYearStart - val rangeStart = baseDate.toTimestamp() - val rangeEnd = LocalDate.now().plusWeeks(1).toTimestamp() - - val constraintsBuilder = CalendarConstraints.Builder().apply { - setValidator(SchoolDaysValidator(rangeStart, rangeEnd)) - setStart(rangeStart) - setEnd(rangeEnd) - } - val datePicker = MaterialDatePicker.Builder.datePicker() - .setCalendarConstraints(constraintsBuilder.build()) - .setSelection(currentDate.toTimestamp()) - .build() - - datePicker.addOnPositiveButtonClickListener { - val date = it.toLocalDateTime() - presenter.onDateSet(date.year, date.monthValue, date.dayOfMonth) - } - - if (!parentFragmentManager.isStateSaved) { - datePicker.show(parentFragmentManager, null) - } + override fun showDatePickerDialog(selectedDate: LocalDate) { + openMaterialDatePicker( + selected = selectedDate, + rangeStart = selectedDate.firstSchoolDayInSchoolYear, + rangeEnd = LocalDate.now().plusWeeks(1), + onDateSelected = { + presenter.onDateSet(it.year, it.monthValue, it.dayOfMonth) + } + ) } override fun showExcuseDialog() { 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 54d29bcf8..26bfaf19f 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendancePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendancePresenter.kt @@ -1,7 +1,7 @@ package io.github.wulkanowy.ui.modules.attendance import android.annotation.SuppressLint -import io.github.wulkanowy.data.Status +import io.github.wulkanowy.data.* import io.github.wulkanowy.data.db.entities.Attendance import io.github.wulkanowy.data.repositories.AttendanceRepository import io.github.wulkanowy.data.repositories.PreferencesRepository @@ -9,18 +9,7 @@ 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.AnalyticsHelper -import io.github.wulkanowy.utils.afterLoading -import io.github.wulkanowy.utils.capitalise -import io.github.wulkanowy.utils.flowWithResource -import io.github.wulkanowy.utils.flowWithResourceIn -import io.github.wulkanowy.utils.getLastSchoolDayIfHoliday -import io.github.wulkanowy.utils.isExcusableOrNotExcused -import io.github.wulkanowy.utils.isHolidays -import io.github.wulkanowy.utils.nextSchoolDay -import io.github.wulkanowy.utils.previousOrSameSchoolDay -import io.github.wulkanowy.utils.previousSchoolDay -import io.github.wulkanowy.utils.toFormattedString +import io.github.wulkanowy.utils.* import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.onEach @@ -102,15 +91,19 @@ class AttendancePresenter @Inject constructor( fun onViewReselected() { Timber.i("Attendance view is reselected") - view?.also { view -> + view?.let { view -> if (view.currentStackSize == 1) { - baseDate.also { - if (currentDate != it) { - reloadView(it) - loadData() - } else if (!view.isViewEmpty) view.resetView() + baseDate = now().previousOrSameSchoolDay + + if (currentDate != baseDate) { + reloadView(baseDate) + loadData() + } else if (!view.isViewEmpty) { + view.resetView() } - } else view.popView() + } else { + view.popView() + } } } @@ -213,93 +206,77 @@ class AttendancePresenter @Inject constructor( var isParent = false - flowWithResourceIn { + flatResourceFlow { val student = studentRepository.getCurrentStudent() isParent = student.isParent val semester = semesterRepository.getCurrentSemester(student) attendanceRepository.getAttendance( - student, - semester, - currentDate, - currentDate, - forceRefresh + student = student, + semester = semester, + start = currentDate, + end = currentDate, + forceRefresh = forceRefresh ) - }.onEach { - when (it.status) { - Status.LOADING -> { - view?.showExcuseButton(false) - if (!it.data.isNullOrEmpty()) { - val filteredAttendance = if (prefRepository.isShowPresent) { - it.data - } else { - it.data.filter { item -> !item.presence } - } - - view?.run { - enableSwipe(true) - showRefresh(true) - showProgress(false) - showErrorView(false) - showEmpty(filteredAttendance.isEmpty()) - showContent(filteredAttendance.isNotEmpty()) - updateData(filteredAttendance.sortedBy { item -> item.number }) - } - } - } - Status.SUCCESS -> { - Timber.i("Loading attendance result: Success") - val filteredAttendance = if (prefRepository.isShowPresent) { - it.data.orEmpty() - } else { - it.data?.filter { item -> !item.presence }.orEmpty() - } - - isVulcanExcusedFunctionEnabled = - filteredAttendance.any { item -> item.excusable } - - view?.apply { - updateData(filteredAttendance.sortedBy { item -> item.number }) - showEmpty(filteredAttendance.isEmpty()) - showErrorView(false) - showContent(filteredAttendance.isNotEmpty()) - val anyExcusables = filteredAttendance.any { it.isExcusableOrNotExcused } - showExcuseButton(anyExcusables && (isParent || isVulcanExcusedFunctionEnabled)) - } - analytics.logEvent( - "load_data", - "type" to "attendance", - "items" to it.data!!.size - ) - } - Status.ERROR -> { - Timber.i("Loading attendance result: An exception occurred") - errorHandler.dispatch(it.error!!) + } + .logResourceStatus("load attendance") + .onResourceLoading { + view?.showExcuseButton(false) + } + .mapResourceData { + if (prefRepository.isShowPresent) { + it + } else { + it.filter { item -> !item.presence } + }.sortedBy { item -> item.number } + } + .onResourceData { + view?.run { + enableSwipe(true) + showProgress(false) + showErrorView(false) + showEmpty(it.isEmpty()) + showContent(it.isNotEmpty()) + updateData(it) } } - }.afterLoading { - view?.run { - showRefresh(false) - showProgress(false) - enableSwipe(true) + .onResourceIntermediate { view?.showRefresh(true) } + .onResourceSuccess { + isVulcanExcusedFunctionEnabled = it.any { item -> item.excusable } + val anyExcusables = it.any { it.isExcusableOrNotExcused } + view?.showExcuseButton(anyExcusables && (isParent || isVulcanExcusedFunctionEnabled)) + + analytics.logEvent( + "load_data", + "type" to "attendance", + "items" to it.size + ) } - }.launch() + .onResourceNotLoading { + view?.run { + showRefresh(false) + showProgress(false) + enableSwipe(true) + } + } + .onResourceError(errorHandler::dispatch) + .launch() } private fun excuseAbsence(reason: String?, toExcuseList: List) { - flowWithResource { + resourceFlow { val student = studentRepository.getCurrentStudent() val semester = semesterRepository.getCurrentSemester(student) attendanceRepository.excuseForAbsence(student, semester, toExcuseList, reason) }.onEach { - when (it.status) { - Status.LOADING -> view?.run { + when (it) { + is Resource.Loading -> view?.run { Timber.i("Excusing absence started") showProgress(true) showContent(false) showExcuseButton(false) } - Status.SUCCESS -> { + is Resource.Success -> { Timber.i("Excusing for absence result: Success") analytics.logEvent("excuse_absence", "items" to attendanceToExcuseList.size) attendanceToExcuseList.clear() @@ -311,9 +288,9 @@ class AttendancePresenter @Inject constructor( } loadData(forceRefresh = true) } - Status.ERROR -> { + is Resource.Error -> { Timber.i("Excusing for absence result: An exception occurred") - errorHandler.dispatch(it.error!!) + errorHandler.dispatch(it.error) loadData() } } 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 7ddd75f48..b0123065a 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 @@ -48,7 +48,7 @@ interface AttendanceView : BaseView { fun showAttendanceDialog(lesson: Attendance) - fun showDatePickerDialog(currentDate: LocalDate) + fun showDatePickerDialog(selectedDate: LocalDate) fun showExcuseDialog() 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 dd1644a98..e750b8d57 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 @@ -74,7 +74,7 @@ class AttendanceSummaryFragment : binding.attendanceSummarySubjectsContainer.elevation = requireContext().dpToPx(1f) } - override fun updateSubjects(data: ArrayList) { + override fun updateSubjects(data: Collection) { with(subjectsAdapter) { clear() addAll(data) 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 8b603837b..281999176 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 @@ -1,6 +1,6 @@ package io.github.wulkanowy.ui.modules.attendance.summary -import io.github.wulkanowy.data.Status +import io.github.wulkanowy.data.* import io.github.wulkanowy.data.db.entities.AttendanceSummary import io.github.wulkanowy.data.db.entities.Subject import io.github.wulkanowy.data.repositories.AttendanceSummaryRepository @@ -10,9 +10,6 @@ import io.github.wulkanowy.data.repositories.SubjectRepository 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.afterLoading -import io.github.wulkanowy.utils.flowWithResourceIn -import kotlinx.coroutines.flow.onEach import timber.log.Timber import java.time.Month import javax.inject.Inject @@ -75,11 +72,9 @@ class AttendanceSummaryPresenter @Inject constructor( } private fun loadData(subjectId: Int, forceRefresh: Boolean = false) { - Timber.i("Loading attendance summary data started") - currentSubjectId = subjectId - flowWithResourceIn { + flatResourceFlow { val student = studentRepository.getCurrentStudent() val semester = semesterRepository.getCurrentSemester(student) @@ -89,47 +84,37 @@ class AttendanceSummaryPresenter @Inject constructor( subjectId = subjectId, forceRefresh = forceRefresh ) - }.onEach { - when (it.status) { - Status.LOADING -> { - if (!it.data.isNullOrEmpty()) { - view?.run { - enableSwipe(true) - showRefresh(true) - showProgress(false) - showContent(true) - showErrorView(false) - updateDataSet(sortItems(it.data)) - } - } - } - 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)) - } - analytics.logEvent( - "load_data", - "type" to "attendance_summary", - "items" to it.data!!.size, - "item_id" to subjectId - ) - } - Status.ERROR -> { - Timber.i("Loading attendance summary result: An exception occurred") - errorHandler.dispatch(it.error!!) + } + .logResourceStatus("load attendance summary") + .mapResourceData(this::sortItems) + .onResourceData { + view?.run { + enableSwipe(true) + showProgress(false) + showErrorView(false) + showContent(it.isNotEmpty()) + showEmpty(it.isEmpty()) + updateDataSet(it) } } - }.afterLoading { - view?.run { - showRefresh(false) - showProgress(false) - enableSwipe(true) + .onResourceIntermediate { view?.showRefresh(true) } + .onResourceSuccess { + analytics.logEvent( + "load_data", + "type" to "attendance_summary", + "items" to it.size, + "item_id" to subjectId + ) } - }.launch() + .onResourceNotLoading { + view?.run { + showProgress(false) + showRefresh(false) + enableSwipe(true) + } + } + .onResourceError(errorHandler::dispatch) + .launch() } private fun sortItems(items: List) = items.sortedByDescending { item -> @@ -148,27 +133,20 @@ class AttendanceSummaryPresenter @Inject constructor( } private fun loadSubjects() { - flowWithResourceIn { + flatResourceFlow { val student = studentRepository.getCurrentStudent() val semester = semesterRepository.getCurrentSemester(student) subjectRepository.getSubjects(student, semester) - }.onEach { - when (it.status) { - Status.LOADING -> Timber.i("Loading attendance summary subjects started") - Status.SUCCESS -> { - subjects = it.data!! - - Timber.i("Loading attendance summary subjects result: Success") - view?.run { - view?.updateSubjects(ArrayList(it.data.map { subject -> subject.name })) - showSubjects(true) - } - } - Status.ERROR -> { - Timber.i("Loading attendance summary subjects result: An exception occurred") - errorHandler.dispatch(it.error!!) + } + .logResourceStatus("load attendance summary subjects") + .onResourceData { + subjects = it + view?.run { + view?.updateSubjects(it.map { subject -> subject.name }.toList()) + showSubjects(true) } } - }.launch("subjects") + .onResourceError(errorHandler::dispatch) + .launch("subjects") } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/summary/AttendanceSummaryView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/summary/AttendanceSummaryView.kt index 66f370c5c..99192f185 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/summary/AttendanceSummaryView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/summary/AttendanceSummaryView.kt @@ -25,7 +25,7 @@ interface AttendanceSummaryView : BaseView { fun updateDataSet(data: List) - fun updateSubjects(data: ArrayList) + fun updateSubjects(data: Collection) fun showSubjects(show: Boolean) 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 dab170daa..f53648930 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,6 @@ package io.github.wulkanowy.ui.modules.conference -import io.github.wulkanowy.data.Status +import io.github.wulkanowy.data.* import io.github.wulkanowy.data.db.entities.Conference import io.github.wulkanowy.data.repositories.ConferenceRepository import io.github.wulkanowy.data.repositories.SemesterRepository @@ -8,9 +8,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.afterLoading -import io.github.wulkanowy.utils.flowWithResourceIn -import kotlinx.coroutines.flow.onEach import timber.log.Timber import javax.inject.Inject @@ -64,50 +61,39 @@ class ConferencePresenter @Inject constructor( } private fun loadData(forceRefresh: Boolean = false) { - Timber.i("Loading conference data started") - - flowWithResourceIn { + flatResourceFlow { val student = studentRepository.getCurrentStudent() val semester = semesterRepository.getCurrentSemester(student) conferenceRepository.getConferences(student, semester, forceRefresh) - }.onEach { - when (it.status) { - Status.LOADING -> { - if (!it.data.isNullOrEmpty()) { - view?.run { - enableSwipe(true) - showRefresh(true) - showProgress(false) - showContent(true) - updateData(it.data.sortedByDescending { conference -> conference.date }) - } - } - } - Status.SUCCESS -> { - Timber.i("Loading conference result: Success") - view?.run { - updateData(it.data!!.sortedByDescending { conference -> conference.date }) - showContent(it.data.isNotEmpty()) - showEmpty(it.data.isEmpty()) - showErrorView(false) - } - analytics.logEvent( - "load_data", - "type" to "conferences", - "items" to it.data!!.size - ) - } - Status.ERROR -> { - Timber.i("Loading conference result: An exception occurred") - errorHandler.dispatch(it.error!!) + } + .logResourceStatus("load conference data") + .mapResourceData { it.sortedByDescending { conference -> conference.date } } + .onResourceData { + view?.run { + enableSwipe(true) + showProgress(false) + showErrorView(false) + showContent(it.isNotEmpty()) + showEmpty(it.isEmpty()) + updateData(it) } } - }.afterLoading { - view?.run { - showRefresh(false) - showProgress(false) - enableSwipe(true) + .onResourceIntermediate { view?.showRefresh(true) } + .onResourceSuccess { + analytics.logEvent( + "load_data", + "type" to "conferences", + "items" to it.size + ) } - }.launch() + .onResourceNotLoading { + view?.run { + enableSwipe(true) + showProgress(false) + showRefresh(false) + } + } + .onResourceError(errorHandler::dispatch) + .launch() } } 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 12a28ea73..de0b4a6c9 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardFragment.kt @@ -18,6 +18,7 @@ import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.modules.account.accountdetails.AccountDetailsFragment import io.github.wulkanowy.ui.modules.attendance.summary.AttendanceSummaryFragment import io.github.wulkanowy.ui.modules.conference.ConferenceFragment +import io.github.wulkanowy.ui.modules.dashboard.adapters.DashboardAdapter import io.github.wulkanowy.ui.modules.exam.ExamFragment import io.github.wulkanowy.ui.modules.grade.GradeFragment import io.github.wulkanowy.ui.modules.homework.HomeworkFragment @@ -28,10 +29,7 @@ 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 io.github.wulkanowy.utils.* import java.time.LocalDate import javax.inject.Inject @@ -50,6 +48,14 @@ class DashboardFragment : BaseFragment(R.layout.fragme override var subtitleString = LocalDate.now().toFormattedString("EEEE, d MMMM yyyy").capitalise() + override val tileWidth: Int + get() { + val recyclerWidth = binding.dashboardRecycler.width + val margin = requireContext().dpToPx(24f).toInt() + + return ((recyclerWidth - margin) / resources.displayMetrics.density).toInt() + } + companion object { fun newInstance() = DashboardFragment() @@ -178,8 +184,8 @@ class DashboardFragment : BaseFragment(R.layout.fragme binding.dashboardErrorContainer.isVisible = show } - override fun setErrorDetails(message: String) { - binding.dashboardErrorMessage.text = message + override fun setErrorDetails(error: Throwable) { + binding.dashboardErrorMessage.text = requireContext().resources.getErrorString(error) } override fun resetView() { @@ -207,4 +213,4 @@ class DashboardFragment : BaseFragment(R.layout.fragme presenter.onDetachView() super.onDestroyView() } -} \ No newline at end of file +} 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 c20bae7fa..e220ae236 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,13 +1,9 @@ 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 -import io.github.wulkanowy.data.db.entities.SchoolAnnouncement -import io.github.wulkanowy.data.db.entities.Student +import io.github.wulkanowy.data.db.entities.* import io.github.wulkanowy.data.enums.GradeColorTheme import io.github.wulkanowy.data.pojos.TimetableFull +import io.github.wulkanowy.utils.AdBanner import io.github.wulkanowy.data.db.entities.Homework as EntitiesHomework sealed class DashboardItem(val type: Type) { @@ -106,17 +102,26 @@ sealed class DashboardItem(val type: Type) { override val isDataLoaded get() = conferences != null } + data class Ads( + val adBanner: AdBanner? = null, + override val error: Throwable? = null, + override val isLoading: Boolean = false + ) : DashboardItem(Type.ADS) { + + override val isDataLoaded get() = adBanner != null + } + enum class Type { ADMIN_MESSAGE, ACCOUNT, HORIZONTAL_GROUP, LESSONS, + ADS, GRADES, HOMEWORK, ANNOUNCEMENTS, EXAMS, CONFERENCES, - ADS } enum class Tile { @@ -126,12 +131,12 @@ sealed class DashboardItem(val type: Type) { MESSAGES, ATTENDANCE, LESSONS, + ADS, GRADES, HOMEWORK, ANNOUNCEMENTS, EXAMS, CONFERENCES, - ADS } } @@ -148,4 +153,4 @@ fun DashboardItem.Tile.toDashboardItemType() = when (this) { DashboardItem.Tile.EXAMS -> DashboardItem.Type.EXAMS DashboardItem.Tile.CONFERENCES -> DashboardItem.Type.CONFERENCES DashboardItem.Tile.ADS -> DashboardItem.Type.ADS -} \ No newline at end of file +} 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 b9625570f..9c15acc35 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 @@ -2,7 +2,8 @@ package io.github.wulkanowy.ui.modules.dashboard import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.RecyclerView -import java.util.Collections +import io.github.wulkanowy.ui.modules.dashboard.adapters.DashboardAdapter +import java.util.* class DashboardItemMoveCallback( private val dashboardAdapter: DashboardAdapter, 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 d081e19aa..350300937 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 @@ -1,42 +1,21 @@ package io.github.wulkanowy.ui.modules.dashboard -import io.github.wulkanowy.data.Resource -import io.github.wulkanowy.data.Status +import io.github.wulkanowy.data.* import io.github.wulkanowy.data.db.entities.AdminMessage 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 -import io.github.wulkanowy.data.repositories.GradeRepository -import io.github.wulkanowy.data.repositories.HomeworkRepository -import io.github.wulkanowy.data.repositories.LuckyNumberRepository -import io.github.wulkanowy.data.repositories.MessageRepository -import io.github.wulkanowy.data.repositories.PreferencesRepository -import io.github.wulkanowy.data.repositories.SchoolAnnouncementRepository -import io.github.wulkanowy.data.repositories.SemesterRepository -import io.github.wulkanowy.data.repositories.StudentRepository -import io.github.wulkanowy.data.repositories.TimetableRepository +import io.github.wulkanowy.data.repositories.* import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.ErrorHandler +import io.github.wulkanowy.utils.AdsHelper import io.github.wulkanowy.utils.calculatePercentage -import io.github.wulkanowy.utils.flowWithResourceIn import io.github.wulkanowy.utils.nextOrSameSchoolDay -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.emitAll -import kotlinx.coroutines.flow.filterNot -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import timber.log.Timber +import java.time.Instant import java.time.LocalDate -import java.time.LocalDateTime import javax.inject.Inject class DashboardPresenter @Inject constructor( @@ -46,6 +25,7 @@ class DashboardPresenter @Inject constructor( private val gradeRepository: GradeRepository, private val semesterRepository: SemesterRepository, private val messageRepository: MessageRepository, + private val mailboxRepository: MailboxRepository, private val attendanceSummaryRepository: AttendanceSummaryRepository, private val timetableRepository: TimetableRepository, private val homeworkRepository: HomeworkRepository, @@ -53,7 +33,8 @@ class DashboardPresenter @Inject constructor( private val conferenceRepository: ConferenceRepository, private val preferencesRepository: PreferencesRepository, private val schoolAnnouncementRepository: SchoolAnnouncementRepository, - private val adminMessageRepository: AdminMessageRepository + private val adminMessageRepository: AdminMessageRepository, + private val adsHelper: AdsHelper ) : BasePresenter(errorHandler, studentRepository) { private val dashboardItemLoadedList = mutableListOf() @@ -77,7 +58,11 @@ class DashboardPresenter @Inject constructor( showContent(false) } - preferencesRepository.selectedDashboardTilesFlow + merge( + preferencesRepository.selectedDashboardTilesFlow, + preferencesRepository.isAdsEnabledFlow + .map { preferencesRepository.selectedDashboardTiles } + ) .onEach { loadData(tilesToLoad = it) } .launch("dashboard_pref") } @@ -188,7 +173,7 @@ class DashboardPresenter @Inject constructor( DashboardItem.Type.CONFERENCES -> { loadConferences(student, forceRefresh) } - DashboardItem.Type.ADS -> TODO() + DashboardItem.Type.ADS -> loadAds(forceRefresh) DashboardItem.Type.ADMIN_MESSAGE -> loadAdminMessage(student, forceRefresh) } } @@ -243,29 +228,29 @@ class DashboardPresenter @Inject constructor( private fun loadHorizontalGroup(student: Student, forceRefresh: Boolean) { flow { val semester = semesterRepository.getCurrentSemester(student) + val mailbox = mailboxRepository.getMailbox(student) val selectedTiles = preferencesRepository.selectedDashboardTiles + val flowSuccess = flowOf(Resource.Success(null)) val luckyNumberFlow = luckyNumberRepository.getLuckyNumber(student, forceRefresh) - .map { - if (it.data == null) { - it.copy(data = LuckyNumber(0, LocalDate.now(), 0)) - } else it + .mapResourceData { + it ?: LuckyNumber(0, LocalDate.now(), 0) } - .takeIf { DashboardItem.Tile.LUCKY_NUMBER in selectedTiles } ?: flowOf(null) + .takeIf { DashboardItem.Tile.LUCKY_NUMBER in selectedTiles } ?: flowSuccess val messageFLow = messageRepository.getMessages( student = student, - semester = semester, + mailbox = mailbox, folder = MessageFolder.RECEIVED, forceRefresh = forceRefresh - ).takeIf { DashboardItem.Tile.MESSAGES in selectedTiles } ?: flowOf(null) + ).takeIf { DashboardItem.Tile.MESSAGES in selectedTiles } ?: flowSuccess val attendanceFlow = attendanceSummaryRepository.getAttendanceSummary( student = student, semester = semester, subjectId = -1, forceRefresh = forceRefresh - ).takeIf { DashboardItem.Tile.ATTENDANCE in selectedTiles } ?: flowOf(null) + ).takeIf { DashboardItem.Tile.ATTENDANCE in selectedTiles } ?: flowSuccess emitAll( combine( @@ -273,16 +258,13 @@ class DashboardPresenter @Inject constructor( messageFLow, attendanceFlow ) { luckyNumberResource, messageResource, attendanceResource -> - val error = - luckyNumberResource?.error ?: messageResource?.error ?: attendanceResource?.error - error?.let { throw it } + val resList = listOf(luckyNumberResource, messageResource, attendanceResource) + resList.firstNotNullOfOrNull { it.errorOrNull }?.let { throw it } + val isLoading = resList.any { it is Resource.Loading } - val luckyNumber = luckyNumberResource?.data?.luckyNumber - val messageCount = messageResource?.data?.count { it.unread } - val attendancePercentage = attendanceResource?.data?.calculatePercentage() - - val isLoading = - luckyNumberResource?.status == Status.LOADING || messageResource?.status == Status.LOADING || attendanceResource?.status == Status.LOADING + val luckyNumber = luckyNumberResource.dataOrNull?.luckyNumber + val messageCount = messageResource.dataOrNull?.count { it.unread } + val attendancePercentage = attendanceResource.dataOrNull?.calculatePercentage() DashboardItem.HorizontalGroup( isLoading = isLoading, @@ -315,72 +297,69 @@ class DashboardPresenter @Inject constructor( ) errorHandler.dispatch(it) } - .launch("horizontal_group") + .launch("horizontal_group ${if (forceRefresh) "-forceRefresh" else ""}") } private fun loadGrades(student: Student, forceRefresh: Boolean) { - flowWithResourceIn { + flatResourceFlow { val semester = semesterRepository.getCurrentSemester(student) gradeRepository.getGrades(student, semester, forceRefresh) - }.map { originalResource -> - val filteredSubjectWithGrades = originalResource.data?.first - .orEmpty() - .filter { it.date >= LocalDate.now().minusDays(7) } - .groupBy { it.subject } - .mapValues { entry -> - entry.value - .take(5) - .sortedByDescending { it.date } - } - .toList() - .sortedByDescending { (_, grades) -> grades[0].date } - .toMap() + } + .mapResourceData { (details, _) -> + val filteredSubjectWithGrades = details + .filter { it.date >= LocalDate.now().minusDays(7) } + .groupBy { it.subject } + .mapValues { entry -> + entry.value + .take(5) + .sortedByDescending { it.date } + } + .toList() + .sortedByDescending { (_, grades) -> grades[0].date } + .toMap() - Resource( - status = originalResource.status, - data = filteredSubjectWithGrades.takeIf { originalResource.data != null }, - error = originalResource.error - ) - }.onEach { - when (it.status) { - Status.LOADING -> { - Timber.i("Loading dashboard grades data started") - if (forceRefresh) return@onEach + filteredSubjectWithGrades + } + .onEach { + when (it) { + is Resource.Loading -> { + Timber.i("Loading dashboard grades data started") + if (forceRefresh) return@onEach + updateData( + DashboardItem.Grades( + subjectWithGrades = it.dataOrNull, + gradeTheme = preferencesRepository.gradeColorTheme, + isLoading = true + ), forceRefresh + ) - updateData( - DashboardItem.Grades( - subjectWithGrades = it.data, - gradeTheme = preferencesRepository.gradeColorTheme, - isLoading = true - ), forceRefresh - ) - - if (!it.data.isNullOrEmpty()) { - firstLoadedItemList += DashboardItem.Type.GRADES + if (!it.dataOrNull.isNullOrEmpty()) { + firstLoadedItemList += DashboardItem.Type.GRADES + } + } + is Resource.Success -> { + Timber.i("Loading dashboard grades result: Success") + updateData( + DashboardItem.Grades( + subjectWithGrades = it.data, + gradeTheme = preferencesRepository.gradeColorTheme + ), + forceRefresh + ) + } + is Resource.Error -> { + Timber.i("Loading dashboard grades result: An exception occurred") + errorHandler.dispatch(it.error) + updateData(DashboardItem.Grades(error = it.error), forceRefresh) } } - Status.SUCCESS -> { - Timber.i("Loading dashboard grades result: Success") - updateData( - DashboardItem.Grades( - subjectWithGrades = it.data, - gradeTheme = preferencesRepository.gradeColorTheme - ), - forceRefresh - ) - } - Status.ERROR -> { - Timber.i("Loading dashboard grades result: An exception occurred") - errorHandler.dispatch(it.error!!) - updateData(DashboardItem.Grades(error = it.error), forceRefresh) - } } - }.launch("dashboard_grades") + .launchWithUniqueRefreshJob("dashboard_grades", forceRefresh) } private fun loadLessons(student: Student, forceRefresh: Boolean) { - flowWithResourceIn { + flatResourceFlow { val semester = semesterRepository.getCurrentSemester(student) val date = LocalDate.now().nextOrSameSchoolDay @@ -391,40 +370,41 @@ class DashboardPresenter @Inject constructor( end = date.plusDays(1), forceRefresh = forceRefresh ) + } + .onEach { + when (it) { + is Resource.Loading -> { + Timber.i("Loading dashboard lessons data started") + if (forceRefresh) return@onEach + updateData( + DashboardItem.Lessons(it.dataOrNull, isLoading = true), + forceRefresh + ) - }.onEach { - when (it.status) { - Status.LOADING -> { - Timber.i("Loading dashboard lessons data started") - if (forceRefresh) return@onEach - updateData( - DashboardItem.Lessons(it.data, isLoading = true), - forceRefresh - ) - - if (!it.data?.lessons.isNullOrEmpty()) { - firstLoadedItemList += DashboardItem.Type.LESSONS + if (!it.dataOrNull?.lessons.isNullOrEmpty()) { + firstLoadedItemList += DashboardItem.Type.LESSONS + } + } + is Resource.Success -> { + Timber.i("Loading dashboard lessons result: Success") + updateData( + DashboardItem.Lessons(it.data), forceRefresh + ) + } + is Resource.Error -> { + Timber.i("Loading dashboard lessons result: An exception occurred") + errorHandler.dispatch(it.error) + updateData( + DashboardItem.Lessons(error = it.error), forceRefresh + ) } } - Status.SUCCESS -> { - Timber.i("Loading dashboard lessons result: Success") - updateData( - DashboardItem.Lessons(it.data), forceRefresh - ) - } - Status.ERROR -> { - Timber.i("Loading dashboard lessons result: An exception occurred") - errorHandler.dispatch(it.error!!) - updateData( - DashboardItem.Lessons(error = it.error), forceRefresh - ) - } } - }.launch("dashboard_lessons") + .launchWithUniqueRefreshJob("dashboard_lessons", forceRefresh) } private fun loadHomework(student: Student, forceRefresh: Boolean) { - flowWithResourceIn { + flatResourceFlow { val semester = semesterRepository.getCurrentSemester(student) val date = LocalDate.now().nextOrSameSchoolDay @@ -435,73 +415,79 @@ class DashboardPresenter @Inject constructor( end = date, forceRefresh = forceRefresh ) - }.map { homeworkResource -> - val currentDate = LocalDate.now() + } + .mapResourceData { homework -> + val currentDate = LocalDate.now() - val filteredHomework = homeworkResource.data - ?.filter { (it.date.isAfter(currentDate) || it.date == currentDate) && !it.isDone } - ?.sortedBy { it.date } + val filteredHomework = homework.filter { + (it.date.isAfter(currentDate) || it.date == currentDate) && !it.isDone + }.sortedBy { it.date } - homeworkResource.copy(data = filteredHomework) - }.onEach { - when (it.status) { - Status.LOADING -> { - Timber.i("Loading dashboard homework data started") - if (forceRefresh) return@onEach - updateData( - DashboardItem.Homework(it.data ?: emptyList(), isLoading = true), - forceRefresh - ) + filteredHomework + } + .onEach { + when (it) { + is Resource.Loading -> { + Timber.i("Loading dashboard homework data started") + if (forceRefresh) return@onEach + val data = it.dataOrNull.orEmpty() + updateData( + DashboardItem.Homework(data, isLoading = true), + forceRefresh + ) - if (!it.data.isNullOrEmpty()) { - firstLoadedItemList += DashboardItem.Type.HOMEWORK + if (data.isNotEmpty()) { + firstLoadedItemList += DashboardItem.Type.HOMEWORK + } + } + is Resource.Success -> { + Timber.i("Loading dashboard homework result: Success") + updateData(DashboardItem.Homework(it.data), forceRefresh) + } + is Resource.Error -> { + Timber.i("Loading dashboard homework result: An exception occurred") + errorHandler.dispatch(it.error) + updateData(DashboardItem.Homework(error = it.error), forceRefresh) } } - Status.SUCCESS -> { - Timber.i("Loading dashboard homework result: Success") - updateData(DashboardItem.Homework(it.data ?: emptyList()), forceRefresh) - } - Status.ERROR -> { - Timber.i("Loading dashboard homework result: An exception occurred") - errorHandler.dispatch(it.error!!) - updateData(DashboardItem.Homework(error = it.error), forceRefresh) - } } - }.launch("dashboard_homework") + .launchWithUniqueRefreshJob("dashboard_homework", forceRefresh) } private fun loadSchoolAnnouncements(student: Student, forceRefresh: Boolean) { - flowWithResourceIn { + flatResourceFlow { schoolAnnouncementRepository.getSchoolAnnouncements(student, forceRefresh) - }.onEach { - when (it.status) { - Status.LOADING -> { - Timber.i("Loading dashboard announcements data started") - if (forceRefresh) return@onEach - updateData( - DashboardItem.Announcements(it.data ?: emptyList(), isLoading = true), - forceRefresh - ) + } + .onEach { + when (it) { + is Resource.Loading -> { + Timber.i("Loading dashboard announcements data started") + if (forceRefresh) return@onEach + updateData( + DashboardItem.Announcements(it.dataOrNull.orEmpty(), isLoading = true), + forceRefresh + ) - if (!it.data.isNullOrEmpty()) { - firstLoadedItemList += DashboardItem.Type.ANNOUNCEMENTS + if (!it.dataOrNull.isNullOrEmpty()) { + firstLoadedItemList += DashboardItem.Type.ANNOUNCEMENTS + } + } + is Resource.Success -> { + Timber.i("Loading dashboard announcements result: Success") + updateData(DashboardItem.Announcements(it.data), forceRefresh) + } + is Resource.Error -> { + Timber.i("Loading dashboard announcements result: An exception occurred") + errorHandler.dispatch(it.error) + updateData(DashboardItem.Announcements(error = it.error), forceRefresh) } } - Status.SUCCESS -> { - Timber.i("Loading dashboard announcements result: Success") - updateData(DashboardItem.Announcements(it.data ?: emptyList()), forceRefresh) - } - Status.ERROR -> { - Timber.i("Loading dashboard announcements result: An exception occurred") - errorHandler.dispatch(it.error!!) - updateData(DashboardItem.Announcements(error = it.error), forceRefresh) - } } - }.launch("dashboard_announcements") + .launchWithUniqueRefreshJob("dashboard_announcements", forceRefresh) } private fun loadExams(student: Student, forceRefresh: Boolean) { - flowWithResourceIn { + flatResourceFlow { val semester = semesterRepository.getCurrentSemester(student) examRepository.getExams( @@ -512,101 +498,101 @@ class DashboardPresenter @Inject constructor( forceRefresh = forceRefresh ) } - .map { examResource -> - val sortedExams = examResource.data?.sortedBy { it.date } - - examResource.copy(data = sortedExams) - } + .mapResourceData { exams -> exams.sortedBy { exam -> exam.date } } .onEach { - when (it.status) { - Status.LOADING -> { + when (it) { + is Resource.Loading -> { Timber.i("Loading dashboard exams data started") if (forceRefresh) return@onEach updateData( - DashboardItem.Exams(it.data.orEmpty(), isLoading = true), + DashboardItem.Exams(it.dataOrNull.orEmpty(), isLoading = true), forceRefresh ) - if (!it.data.isNullOrEmpty()) { + if (!it.dataOrNull.isNullOrEmpty()) { firstLoadedItemList += DashboardItem.Type.EXAMS } } - Status.SUCCESS -> { + is Resource.Success -> { Timber.i("Loading dashboard exams result: Success") - updateData(DashboardItem.Exams(it.data ?: emptyList()), forceRefresh) + updateData(DashboardItem.Exams(it.data), forceRefresh) } - Status.ERROR -> { + is Resource.Error -> { Timber.i("Loading dashboard exams result: An exception occurred") - errorHandler.dispatch(it.error!!) + errorHandler.dispatch(it.error) updateData(DashboardItem.Exams(error = it.error), forceRefresh) } } - }.launch("dashboard_exams") + } + .launchWithUniqueRefreshJob("dashboard_exams", forceRefresh) } private fun loadConferences(student: Student, forceRefresh: Boolean) { - flowWithResourceIn { + flatResourceFlow { val semester = semesterRepository.getCurrentSemester(student) conferenceRepository.getConferences( student = student, semester = semester, forceRefresh = forceRefresh, - startDate = LocalDateTime.now() + startDate = Instant.now(), ) - }.onEach { - when (it.status) { - Status.LOADING -> { - Timber.i("Loading dashboard conferences data started") - if (forceRefresh) return@onEach - updateData( - DashboardItem.Conferences(it.data ?: emptyList(), isLoading = true), - forceRefresh - ) + } + .onEach { + when (it) { + is Resource.Loading -> { + Timber.i("Loading dashboard conferences data started") + if (forceRefresh) return@onEach + updateData( + DashboardItem.Conferences(it.dataOrNull.orEmpty(), isLoading = true), + forceRefresh + ) - if (!it.data.isNullOrEmpty()) { - firstLoadedItemList += DashboardItem.Type.CONFERENCES + if (!it.dataOrNull.isNullOrEmpty()) { + firstLoadedItemList += DashboardItem.Type.CONFERENCES + } + } + is Resource.Success -> { + Timber.i("Loading dashboard conferences result: Success") + updateData(DashboardItem.Conferences(it.data), forceRefresh) + } + is Resource.Error -> { + Timber.i("Loading dashboard conferences result: An exception occurred") + errorHandler.dispatch(it.error) + updateData(DashboardItem.Conferences(error = it.error), forceRefresh) } } - Status.SUCCESS -> { - Timber.i("Loading dashboard conferences result: Success") - updateData(DashboardItem.Conferences(it.data ?: emptyList()), forceRefresh) - } - Status.ERROR -> { - Timber.i("Loading dashboard conferences result: An exception occurred") - errorHandler.dispatch(it.error!!) - updateData(DashboardItem.Conferences(error = it.error), forceRefresh) - } } - }.launch("dashboard_conferences") + .launchWithUniqueRefreshJob("dashboard_conferences", forceRefresh) } private fun loadAdminMessage(student: Student, forceRefresh: Boolean) { - flowWithResourceIn { adminMessageRepository.getAdminMessages(student, forceRefresh) } - .map { - val isDismissed = it.data?.id in preferencesRepository.dismissedAdminMessageIds - it.copy(data = it.data.takeUnless { isDismissed }) + flatResourceFlow { adminMessageRepository.getAdminMessages(student) } + .filter { + val data = it.dataOrNull ?: return@filter true + val isDismissed = data.id in preferencesRepository.dismissedAdminMessageIds + !isDismissed } .onEach { - when (it.status) { - Status.LOADING -> { + when (it) { + is Resource.Loading -> { Timber.i("Loading dashboard admin message data started") if (forceRefresh) return@onEach updateData(DashboardItem.AdminMessages(), forceRefresh) } - Status.SUCCESS -> { + is Resource.Success -> { Timber.i("Loading dashboard admin message result: Success") updateData( dashboardItem = DashboardItem.AdminMessages(adminMessage = it.data), forceRefresh = forceRefresh ) } - Status.ERROR -> { + is Resource.Error -> { Timber.i("Loading dashboard admin message result: An exception occurred") - errorHandler.dispatch(it.error!!) + errorHandler.dispatch(it.error) updateData( dashboardItem = DashboardItem.AdminMessages( - adminMessage = it.data, + adminMessage = null, error = it.error ), forceRefresh = forceRefresh @@ -614,7 +600,24 @@ class DashboardPresenter @Inject constructor( } } } - .launch("dashboard_admin_messages") + .launchWithUniqueRefreshJob("dashboard_admin_messages", forceRefresh) + } + + private fun loadAds(forceRefresh: Boolean) { + presenterScope.launch { + if (!forceRefresh) { + updateData(DashboardItem.Ads(), forceRefresh) + } + + val dashboardAdItem = + runCatching { + DashboardItem.Ads(adsHelper.getDashboardTileAdBanner(view!!.tileWidth)) + } + .onFailure { Timber.e(it) } + .getOrElse { DashboardItem.Ads(error = it) } + + updateData(dashboardAdItem, forceRefresh) + } } private fun updateData(dashboardItem: DashboardItem, forceRefresh: Boolean) { @@ -641,6 +644,18 @@ class DashboardPresenter @Inject constructor( } } + if (dashboardItem is DashboardItem.Ads) { + if (!dashboardItem.isDataLoaded) { + dashboardItemsToLoad = dashboardItemsToLoad - DashboardItem.Type.ADS + dashboardTileLoadedList = dashboardTileLoadedList - DashboardItem.Tile.ADS + + dashboardItemLoadedList.removeAll { it.type == DashboardItem.Type.ADS } + } else { + dashboardItemsToLoad = dashboardItemsToLoad + DashboardItem.Type.ADS + dashboardTileLoadedList = dashboardTileLoadedList + DashboardItem.Tile.ADS + } + } + if (forceRefresh) { updateForceRefreshData(dashboardItem) } else { @@ -716,7 +731,7 @@ class DashboardPresenter @Inject constructor( itemsLoadedList.find { it.type == DashboardItem.Type.ACCOUNT }?.error != null val isGeneralError = filteredItems.none { it.error == null } && filteredItems.isNotEmpty() || isAccountItemError - val errorMessage = itemsLoadedList.map { it.error?.stackTraceToString() }.toString() + val firstError = itemsLoadedList.mapNotNull { it.error }.firstOrNull() val filteredOriginalLoadedList = dashboardItemLoadedList.filterNot { it.type == DashboardItem.Type.ACCOUNT } @@ -726,7 +741,7 @@ class DashboardPresenter @Inject constructor( filteredOriginalLoadedList.none { it.error == null } && filteredOriginalLoadedList.isNotEmpty() || wasAccountItemError if (isGeneralError && isItemsLoaded) { - lastError = Exception(errorMessage) + lastError = requireNotNull(firstError) view?.run { showProgress(false) @@ -734,6 +749,7 @@ class DashboardPresenter @Inject constructor( if ((forceRefresh && wasGeneralError) || !forceRefresh) { showContent(false) showErrorView(true) + setErrorDetails(lastError) } } } @@ -752,4 +768,18 @@ class DashboardPresenter @Inject constructor( dashboardItemsPosition?.getOrDefault(tile.type, defaultPosition) ?: tile.type.ordinal } } -} \ No newline at end of file + + private fun Flow>.launchWithUniqueRefreshJob(name: String, forceRefresh: Boolean) { + val jobName = if (forceRefresh) "$name-forceRefresh" else name + + if (forceRefresh) { + onEach { + if (it is Resource.Success) { + cancelJobs(jobName) + } + }.launch(jobName) + } else { + launch(jobName) + } + } +} 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 730e19a35..767885434 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 @@ -4,6 +4,8 @@ import io.github.wulkanowy.ui.base.BaseView interface DashboardView : BaseView { + val tileWidth: Int + fun initView() fun updateData(data: List) @@ -18,7 +20,7 @@ interface DashboardView : BaseView { fun showErrorView(show: Boolean) - fun setErrorDetails(message: String) + fun setErrorDetails(error: Throwable) fun resetView() @@ -27,4 +29,4 @@ interface DashboardView : BaseView { fun openNotificationsCenterView() fun openInternetBrowser(url: String) -} \ No newline at end of file +} 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/adapters/DashboardAdapter.kt similarity index 96% rename from app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAdapter.kt rename to app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/adapters/DashboardAdapter.kt index 12be144cd..a3c423a8b 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/adapters/DashboardAdapter.kt @@ -1,4 +1,4 @@ -package io.github.wulkanowy.ui.modules.dashboard +package io.github.wulkanowy.ui.modules.dashboard.adapters import android.annotation.SuppressLint import android.content.res.ColorStateList @@ -9,6 +9,7 @@ import android.os.Looper import android.view.LayoutInflater import android.view.ViewGroup import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.text.parseAsHtml import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams import androidx.core.view.updateMarginsRelative @@ -21,26 +22,15 @@ 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.data.enums.GradeColorTheme -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 -import io.github.wulkanowy.databinding.ItemDashboardGradesBinding -import io.github.wulkanowy.databinding.ItemDashboardHomeworkBinding -import io.github.wulkanowy.databinding.ItemDashboardHorizontalGroupBinding -import io.github.wulkanowy.databinding.ItemDashboardLessonsBinding -import io.github.wulkanowy.utils.createNameInitialsDrawable -import io.github.wulkanowy.utils.dpToPx -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 io.github.wulkanowy.databinding.* +import io.github.wulkanowy.ui.modules.dashboard.DashboardItem +import io.github.wulkanowy.utils.* import timber.log.Timber import java.time.Duration +import java.time.Instant import java.time.LocalDate import java.time.LocalDateTime -import java.util.Timer +import java.util.* import javax.inject.Inject import kotlin.concurrent.timer @@ -121,6 +111,9 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter AdminMessageViewHolder( ItemDashboardAdminMessageBinding.inflate(inflater, parent, false) ) + DashboardItem.Type.ADS.ordinal -> AdsViewHolder( + ItemDashboardAdsBinding.inflate(inflater, parent, false) + ) else -> throw IllegalArgumentException() } } @@ -136,6 +129,7 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter bindExamsViewHolder(holder, position) is ConferencesViewHolder -> bindConferencesViewHolder(holder, position) is AdminMessageViewHolder -> bindAdminMessage(holder, position) + is AdsViewHolder -> bindAdsViewHolder(holder, position) } } @@ -291,7 +285,7 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter, private val oldList: List diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAnnouncementsAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/adapters/DashboardAnnouncementsAdapter.kt similarity index 95% rename from app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAnnouncementsAdapter.kt rename to app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/adapters/DashboardAnnouncementsAdapter.kt index 7a4c2b257..da5880153 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAnnouncementsAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/adapters/DashboardAnnouncementsAdapter.kt @@ -1,4 +1,4 @@ -package io.github.wulkanowy.ui.modules.dashboard +package io.github.wulkanowy.ui.modules.dashboard.adapters import android.view.LayoutInflater import android.view.ViewGroup @@ -33,4 +33,4 @@ class DashboardAnnouncementsAdapter : class ViewHolder(val binding: SubitemDashboardAnnouncementsBinding) : RecyclerView.ViewHolder(binding.root) -} \ No newline at end of file +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardConferencesAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/adapters/DashboardConferencesAdapter.kt similarity index 95% rename from app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardConferencesAdapter.kt rename to app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/adapters/DashboardConferencesAdapter.kt index 64cf599c8..1244ff60f 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardConferencesAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/adapters/DashboardConferencesAdapter.kt @@ -1,4 +1,4 @@ -package io.github.wulkanowy.ui.modules.dashboard +package io.github.wulkanowy.ui.modules.dashboard.adapters import android.view.LayoutInflater import android.view.ViewGroup @@ -33,4 +33,4 @@ class DashboardConferencesAdapter : class ViewHolder(val binding: SubitemDashboardConferencesBinding) : RecyclerView.ViewHolder(binding.root) -} \ No newline at end of file +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardExamsAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/adapters/DashboardExamsAdapter.kt similarity index 97% rename from app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardExamsAdapter.kt rename to app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/adapters/DashboardExamsAdapter.kt index 060f224b3..583bf29da 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardExamsAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/adapters/DashboardExamsAdapter.kt @@ -1,4 +1,4 @@ -package io.github.wulkanowy.ui.modules.dashboard +package io.github.wulkanowy.ui.modules.dashboard.adapters import android.annotation.SuppressLint import android.view.LayoutInflater @@ -56,4 +56,4 @@ class DashboardExamsAdapter : class ViewHolder(val binding: SubitemDashboardExamsBinding) : RecyclerView.ViewHolder(binding.root) -} \ No newline at end of file +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardGradesAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/adapters/DashboardGradesAdapter.kt similarity index 96% rename from app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardGradesAdapter.kt rename to app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/adapters/DashboardGradesAdapter.kt index afffcc511..d00df9d41 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardGradesAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/adapters/DashboardGradesAdapter.kt @@ -1,4 +1,4 @@ -package io.github.wulkanowy.ui.modules.dashboard +package io.github.wulkanowy.ui.modules.dashboard.adapters import android.view.LayoutInflater import android.view.ViewGroup diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardHomeworkAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/adapters/DashboardHomeworkAdapter.kt similarity index 97% rename from app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardHomeworkAdapter.kt rename to app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/adapters/DashboardHomeworkAdapter.kt index 55ec90294..8105ff9c8 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardHomeworkAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/adapters/DashboardHomeworkAdapter.kt @@ -1,4 +1,4 @@ -package io.github.wulkanowy.ui.modules.dashboard +package io.github.wulkanowy.ui.modules.dashboard.adapters import android.annotation.SuppressLint import android.view.LayoutInflater @@ -53,4 +53,4 @@ class DashboardHomeworkAdapter : RecyclerView.Adapter Timber.d("Loading logs files started") - Status.SUCCESS -> { - Timber.i("Loading logs files result: ${it.data!!.joinToString { file -> file.name }}") - view?.shareLogs(it.data) - } - Status.ERROR -> { - Timber.i("Loading logs files result: An exception occurred") - errorHandler.dispatch(it.error!!) + resourceFlow { loggerRepository.getLogFiles() } + .onEach { + when (it) { + is Resource.Loading -> Timber.d("Loading logs files started") + is Resource.Success -> { + Timber.i("Loading logs files result: ${it.data.joinToString { file -> file.name }}") + view?.shareLogs(it.data) + } + is Resource.Error -> { + Timber.i("Loading logs files result: An exception occurred") + errorHandler.dispatch(it.error) + } } } - }.launch("share") + .launch("share") return true } @@ -44,18 +46,20 @@ class LogViewerPresenter @Inject constructor( } private fun loadLogFile() { - flowWithResource { loggerRepository.getLastLogLines() }.onEach { - when (it.status) { - Status.LOADING -> Timber.d("Loading last log file started") - Status.SUCCESS -> { - Timber.i("Loading last log file result: load ${it.data!!.size} lines") - view?.setLines(it.data) - } - Status.ERROR -> { - Timber.i("Loading last log file result: An exception occurred") - errorHandler.dispatch(it.error!!) + resourceFlow { loggerRepository.getLastLogLines() } + .onEach { + when (it) { + is Resource.Loading -> Timber.d("Loading last log file started") + is Resource.Success -> { + Timber.i("Loading last log file result: load ${it.data.size} lines") + view?.setLines(it.data) + } + is Resource.Error -> { + Timber.i("Loading last log file result: An exception occurred") + errorHandler.dispatch(it.error) + } } } - }.launch("file") + .launch("file") } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/mock/conference.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/mock/conference.kt index 40af6bfbb..625ff4c9d 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/mock/conference.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/mock/conference.kt @@ -1,7 +1,8 @@ package io.github.wulkanowy.ui.modules.debug.notification.mock import io.github.wulkanowy.data.db.entities.Conference -import java.time.LocalDateTime +import java.time.Duration +import java.time.Instant val debugConferenceItems = listOf( generateConference( @@ -53,6 +54,6 @@ private fun generateConference(title: String, subject: String) = Conference( diaryId = 0, agenda = "", conferenceId = 0, - date = LocalDateTime.now().plusMinutes(10), + date = Instant.now().plus(Duration.ofMinutes(10)), presentOnConference = "", ) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/mock/gradeDetails.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/mock/gradeDetails.kt index f9c481e39..77b60188b 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/mock/gradeDetails.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/mock/gradeDetails.kt @@ -5,7 +5,7 @@ import java.time.LocalDate val debugGradeDetailsItems = listOf( generateGrade("Matematyka", "+"), - generateGrade("Matematyka", "2="), + generateGrade("Matematyka", "120", comment = "%"), generateGrade("Fizyka", "-"), generateGrade("Geografia", "4+"), generateGrade("Sieci komputerowe", "1"), @@ -17,14 +17,14 @@ val debugGradeDetailsItems = listOf( generateGrade("Wychowanie fizyczne", "5"), ) -private fun generateGrade(subject: String, entry: String) = Grade( +private fun generateGrade(subject: String, entry: String, comment: String = "") = Grade( subject = subject, entry = entry, semesterId = 0, studentId = 0, value = 0.0, modifier = 0.0, - comment = "", + comment = comment, color = "", gradeSymbol = "", description = "", diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/mock/message.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/mock/message.kt index f506d2f6e..6ff26162b 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/mock/message.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/mock/message.kt @@ -1,7 +1,7 @@ package io.github.wulkanowy.ui.modules.debug.notification.mock import io.github.wulkanowy.data.db.entities.Message -import java.time.LocalDateTime +import java.time.Instant val debugMessageItems = listOf( generateMessage("Kowalski Jan", "Tytuł"), @@ -17,16 +17,13 @@ val debugMessageItems = listOf( ) private fun generateMessage(sender: String, subject: String) = Message( - sender = sender, subject = subject, - studentId = 0, - realId = 0, - messageId = 0, - senderId = 0, - recipient = "", - date = LocalDateTime.now(), + messageId = 123, + date = Instant.now(), folderId = 0, unread = true, - removed = false, - hasAttachments = false + hasAttachments = false, + messageGlobalKey = "", + correspondents = sender, + mailboxKey = "", ) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/mock/schoolAnnouncement.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/mock/schoolAnnouncement.kt index 42524e6e0..e2dc5cd84 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/mock/schoolAnnouncement.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/mock/schoolAnnouncement.kt @@ -4,13 +4,13 @@ import io.github.wulkanowy.data.db.entities.SchoolAnnouncement import java.time.LocalDate val debugSchoolAnnouncementItems = listOf( - generateAnnouncement("Dzień wolny od zajęć dydaktycznych", "Dzień wolny od zajęć dydaktycznych\n03.05.2021 - poniedziałek"), + generateAnnouncement("Dzień wolny od zajęć dydaktycznych", "Dzień wolny od zajęć dydaktycznych
03.05.2021 – poniedziałek"), generateAnnouncement("Zasady bezpieczeństwa", "Wszyscy uczniowie są zobowiązani do noszenia maseczek"), generateAnnouncement("Święto szkoły", "W najbliższych dniach obchodzimy święto szkoły, podczas którego..."), generateAnnouncement("Rocznica odzyskania przez szkołę sztandaru", "Juz niedługo, bo za tydzień, a dokładnie za 8 dni..."), generateAnnouncement("Ogłoszenie w sprawie otwarcia stołówki", "Wszyscy uczniowie zainteresowani obiadami w szkole..."), generateAnnouncement("Uczniowie proszeni do sekretariatu", "Kuba i Jacek z klasy czwartej proszeni do dyrektora w trybie pilnym"), - generateAnnouncement("Dzień wolny od zajęć dydaktycznych", "Dzień wolny od zajęć dydaktycznych\n21.06.2021 - poniedziałek"), + generateAnnouncement("Dzień wolny od zajęć dydaktycznych", "Dzień wolny od zajęć dydaktycznych
21.06.2021 – poniedziałek"), generateAnnouncement("Zasady bezpieczeństwa", "Wszyscy uczniowie są zobowiązani do zdjęcia maseczek"), generateAnnouncement("Święto państwowe", "W najbliższych dniach obchodzimy święto państwowe, podczas którego..."), generateAnnouncement("Uczniowie proszeni do sekretariatu", "Kuba i Jacek z klasy czwartej proszeni do dyrektora w trybie wolnym"), @@ -19,6 +19,6 @@ val debugSchoolAnnouncementItems = listOf( private fun generateAnnouncement(subject: String, content: String) = SchoolAnnouncement( subject = subject, content = content, - studentId = 0, + userLoginId = 0, date = LocalDate.now() ) 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 index 428c001d6..ff968654d 100644 --- 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 @@ -1,8 +1,9 @@ package io.github.wulkanowy.ui.modules.debug.notification.mock import io.github.wulkanowy.data.db.entities.Timetable +import java.time.Duration +import java.time.Instant import java.time.LocalDate -import java.time.LocalDateTime import kotlin.random.Random val debugTimetableItems = listOf( @@ -24,8 +25,8 @@ private fun generateTimetable(subject: String, room: String, roomOld: String) = diaryId = 0, date = LocalDate.now().minusDays(Random.nextLong(0, 8)), number = 1, - start = LocalDateTime.now().plusHours(1), - end = LocalDateTime.now(), + start = Instant.now().plus(Duration.ofHours(1)), + end = Instant.now(), subjectOld = "", group = "", room = room, diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamDialog.kt index 3f815a2cb..41adc008a 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamDialog.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamDialog.kt @@ -5,10 +5,13 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.DialogFragment +import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Exam import io.github.wulkanowy.databinding.DialogExamBinding import io.github.wulkanowy.utils.lifecycleAwareVariable +import io.github.wulkanowy.utils.openCalendarEventAdd import io.github.wulkanowy.utils.toFormattedString +import java.time.LocalTime class ExamDialog : DialogFragment() { @@ -46,10 +49,21 @@ class ExamDialog : DialogFragment() { examDialogSubjectValue.text = exam.subject examDialogTypeValue.text = exam.type examDialogTeacherValue.text = exam.teacher - examDialogDateValue.text = exam.entryDate.toFormattedString() - examDialogDescriptionValue.text = exam.description + examDialogEntryDateValue.text = exam.entryDate.toFormattedString() + examDialogDeadlineDateValue.text = exam.date.toFormattedString() + examDialogDescriptionValue.text = exam.description.ifBlank { + getString(R.string.all_no_data) + } examDialogClose.setOnClickListener { dismiss() } + examDialogAddToCalendar.setOnClickListener { + requireContext().openCalendarEventAdd( + title = "${exam.subject} - ${exam.type}", + description = exam.description, + start = exam.date.atTime(LocalTime.of(8, 0)), + end = exam.date.atTime(LocalTime.of(8, 45)), + ) + } } } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamPresenter.kt index 582641fcf..99b0bcb87 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamPresenter.kt @@ -1,21 +1,13 @@ package io.github.wulkanowy.ui.modules.exam -import io.github.wulkanowy.data.Status +import io.github.wulkanowy.data.* import io.github.wulkanowy.data.db.entities.Exam import io.github.wulkanowy.data.repositories.ExamRepository 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.AnalyticsHelper -import io.github.wulkanowy.utils.afterLoading -import io.github.wulkanowy.utils.flowWithResourceIn -import io.github.wulkanowy.utils.getLastSchoolDayIfHoliday -import io.github.wulkanowy.utils.isHolidays -import io.github.wulkanowy.utils.monday -import io.github.wulkanowy.utils.nextOrSameSchoolDay -import io.github.wulkanowy.utils.sunday -import io.github.wulkanowy.utils.toFormattedString +import io.github.wulkanowy.utils.* import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.onEach @@ -86,61 +78,57 @@ class ExamPresenter @Inject constructor( flow { val student = studentRepository.getCurrentStudent() emit(semesterRepository.getCurrentSemester(student)) - }.catch { - Timber.i("Loading semester result: An exception occurred") - }.onEach { - baseDate = baseDate.getLastSchoolDayIfHoliday(it.schoolYear) - currentDate = baseDate - reloadNavigation() - }.launch("holidays") + } + .catch { Timber.i("Loading semester result: An exception occurred") } + .onEach { + baseDate = baseDate.getLastSchoolDayIfHoliday(it.schoolYear) + currentDate = baseDate + reloadNavigation() + } + .launch("holidays") } private fun loadData(forceRefresh: Boolean = false) { - Timber.i("Loading exam data started") - - flowWithResourceIn { + flatResourceFlow { val student = studentRepository.getCurrentStudent() val semester = semesterRepository.getCurrentSemester(student) - examRepository.getExams(student, semester, currentDate.monday, currentDate.sunday, forceRefresh) - }.onEach { - when (it.status) { - Status.LOADING -> { - if (!it.data.isNullOrEmpty()) { - view?.run { - enableSwipe(true) - showRefresh(true) - showProgress(false) - showContent(true) - updateData(createExamItems(it.data)) - } - } - } - Status.SUCCESS -> { - Timber.i("Loading exam result: Success") - view?.apply { - updateData(createExamItems(it.data!!)) - showEmpty(it.data.isEmpty()) - showErrorView(false) - showContent(it.data.isNotEmpty()) - } - analytics.logEvent( - "load_data", - "type" to "exam", - "items" to it.data!!.size - ) - } - Status.ERROR -> { - Timber.i("Loading exam result: An exception occurred") - errorHandler.dispatch(it.error!!) + examRepository.getExams( + student = student, + semester = semester, + start = currentDate.monday, + end = currentDate.sunday, + forceRefresh = forceRefresh + ) + } + .logResourceStatus("load exam data") + .mapResourceData { createExamItems(it) } + .onResourceData { + view?.run { + enableSwipe(true) + showProgress(false) + showErrorView(false) + showContent(it.isNotEmpty()) + showEmpty(it.isEmpty()) + updateData(it) } } - }.afterLoading { - view?.run { - showRefresh(false) - showProgress(false) - enableSwipe(true) + .onResourceIntermediate { view?.showRefresh(true) } + .onResourceSuccess { + analytics.logEvent( + "load_data", + "type" to "exam", + "items" to it.size + ) } - }.launch() + .onResourceNotLoading { + view?.run { + enableSwipe(true) + showProgress(false) + showRefresh(false) + } + } + .onResourceError(errorHandler::dispatch) + .launch() } private fun showErrorViewOnError(message: String, error: Throwable) { @@ -181,8 +169,10 @@ class ExamPresenter @Inject constructor( view?.apply { showPreButton(!currentDate.minusDays(7).isHolidays) showNextButton(!currentDate.plusDays(7).isHolidays) - updateNavigationWeek("${currentDate.monday.toFormattedString("dd.MM")} - " + - currentDate.sunday.toFormattedString("dd.MM")) + updateNavigationWeek( + "${currentDate.monday.toFormattedString("dd.MM")} - " + + currentDate.sunday.toFormattedString("dd.MM") + ) } } } 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 2784f429f..b6733d4f2 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProvider.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProvider.kt @@ -1,7 +1,6 @@ package io.github.wulkanowy.ui.modules.grade -import io.github.wulkanowy.data.Resource -import io.github.wulkanowy.data.Status +import io.github.wulkanowy.data.* import io.github.wulkanowy.data.db.entities.Grade import io.github.wulkanowy.data.db.entities.GradeSummary import io.github.wulkanowy.data.db.entities.Semester @@ -10,17 +9,13 @@ import io.github.wulkanowy.data.repositories.GradeRepository import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.SemesterRepository import io.github.wulkanowy.sdk.Sdk -import io.github.wulkanowy.ui.modules.grade.GradeAverageMode.ALL_YEAR -import io.github.wulkanowy.ui.modules.grade.GradeAverageMode.BOTH_SEMESTERS -import io.github.wulkanowy.ui.modules.grade.GradeAverageMode.ONE_SEMESTER +import io.github.wulkanowy.ui.modules.grade.GradeAverageMode.* import io.github.wulkanowy.utils.calcAverage import io.github.wulkanowy.utils.changeModifier -import io.github.wulkanowy.utils.flowWithResourceIn import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.map import javax.inject.Inject @OptIn(FlowPreview::class) @@ -37,7 +32,7 @@ class GradeAverageProvider @Inject constructor( private val isOptionalArithmeticAverage get() = preferencesRepository.isOptionalArithmeticAverage fun getGradesDetailsWithAverage(student: Student, semesterId: Int, forceRefresh: Boolean) = - flowWithResourceIn { + flatResourceFlow { val semesters = semesterRepository.getSemesters(student) when (preferencesRepository.gradeAverageMode) { @@ -83,17 +78,17 @@ class GradeAverageProvider @Inject constructor( val firstSemesterGradeSubjects = getGradeSubjects(student, firstSemester, forceRefresh) return selectedSemesterGradeSubjects.combine(firstSemesterGradeSubjects) { secondSemesterGradeSubject, firstSemesterGradeSubject -> - if (firstSemesterGradeSubject.status == Status.ERROR) { + if (firstSemesterGradeSubject.errorOrNull != null) { return@combine firstSemesterGradeSubject } val isAnyVulcanAverageInFirstSemester = - firstSemesterGradeSubject.data.orEmpty().any { it.isVulcanAverage } + firstSemesterGradeSubject.dataOrNull.orEmpty().any { it.isVulcanAverage } val isAnyVulcanAverageInSecondSemester = - secondSemesterGradeSubject.data.orEmpty().any { it.isVulcanAverage } + secondSemesterGradeSubject.dataOrNull.orEmpty().any { it.isVulcanAverage } - val updatedData = secondSemesterGradeSubject.data?.map { secondSemesterSubject -> - val firstSemesterSubject = firstSemesterGradeSubject.data.orEmpty() + val updatedData = secondSemesterGradeSubject.dataOrNull?.map { secondSemesterSubject -> + val firstSemesterSubject = firstSemesterGradeSubject.dataOrNull.orEmpty() .singleOrNull { it.subject == secondSemesterSubject.subject } val updatedAverage = if (averageMode == ALL_YEAR) { @@ -115,7 +110,7 @@ class GradeAverageProvider @Inject constructor( } secondSemesterSubject.copy(average = updatedAverage) } - secondSemesterGradeSubject.copy(data = updatedData) + secondSemesterGradeSubject.mapData { updatedData!! } } } @@ -144,20 +139,20 @@ class GradeAverageProvider @Inject constructor( isGradeAverageForceCalc: Boolean, secondSemesterSubject: GradeSubject, firstSemesterSubject: GradeSubject? - ): Double { + ): Double = if (!isAnyVulcanAverage || isGradeAverageForceCalc) { val divider = if (secondSemesterSubject.grades.any { it.weightValue > .0 }) 2 else 1 - return if (!isAnyVulcanAverage || isGradeAverageForceCalc) { - val secondSemesterAverage = - secondSemesterSubject.grades.updateModifiers(student) - .calcAverage(isOptionalArithmeticAverage) - val firstSemesterAverage = firstSemesterSubject?.grades?.updateModifiers(student) - ?.calcAverage(isOptionalArithmeticAverage) ?: secondSemesterAverage + val secondSemesterAverage = secondSemesterSubject.grades.updateModifiers(student) + .calcAverage(isOptionalArithmeticAverage) + val firstSemesterAverage = firstSemesterSubject?.grades?.updateModifiers(student) + ?.calcAverage(isOptionalArithmeticAverage) ?: secondSemesterAverage - (secondSemesterAverage + firstSemesterAverage) / divider - } else { - (secondSemesterSubject.average + (firstSemesterSubject?.average ?: secondSemesterSubject.average)) / divider - } + (secondSemesterAverage + firstSemesterAverage) / divider + } else { + val divider = if (secondSemesterSubject.average > 0) 2 else 1 + + (secondSemesterSubject.average + (firstSemesterSubject?.average + ?: secondSemesterSubject.average)) / divider } private fun getGradeSubjects( @@ -168,17 +163,17 @@ class GradeAverageProvider @Inject constructor( val isGradeAverageForceCalc = preferencesRepository.gradeAverageForceCalc return gradeRepository.getGrades(student, semester, forceRefresh = forceRefresh) - .map { res -> - val (details, summaries) = res.data ?: null to null - val isAnyAverage = summaries.orEmpty().any { it.average != .0 } - val allGrades = details.orEmpty().groupBy { it.subject } + .mapResourceData { res -> + val (details, summaries) = res + val isAnyAverage = summaries.any { it.average != .0 } + val allGrades = details.groupBy { it.subject } - val items = summaries?.emulateEmptySummaries( + val items = summaries.emulateEmptySummaries( student = student, semester = semester, grades = allGrades.toList(), calcAverage = isAnyAverage - )?.map { summary -> + ).map { summary -> val grades = allGrades[summary.subject].orEmpty() GradeSubject( subject = summary.subject, @@ -192,7 +187,7 @@ class GradeAverageProvider @Inject constructor( ) } - Resource(res.status, items, res.error) + items } } 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 76e88bcdb..0ae6521cf 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 @@ -1,16 +1,16 @@ package io.github.wulkanowy.ui.modules.grade -import io.github.wulkanowy.data.Status import io.github.wulkanowy.data.db.entities.Semester +import io.github.wulkanowy.data.logResourceStatus +import io.github.wulkanowy.data.onResourceData +import io.github.wulkanowy.data.onResourceError import io.github.wulkanowy.data.repositories.SemesterRepository import io.github.wulkanowy.data.repositories.StudentRepository +import io.github.wulkanowy.data.resourceFlow import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.utils.AnalyticsHelper -import io.github.wulkanowy.utils.flowWithResource import io.github.wulkanowy.utils.getCurrentOrLast -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.onEach import timber.log.Timber import javax.inject.Inject @@ -99,32 +99,26 @@ class GradePresenter @Inject constructor( } private fun loadData() { - flowWithResource { + resourceFlow { val student = studentRepository.getCurrentStudent() semesterRepository.getSemesters(student, refreshOnNoCurrent = true) - }.onEach { - when (it.status) { - Status.LOADING -> Timber.i("Loading grade data started") - Status.SUCCESS -> { - val current = it.data!!.getCurrentOrLast() - selectedIndex = if (selectedIndex == 0) current.semesterName else selectedIndex - schoolYear = current.schoolYear - semesters = it.data.filter { semester -> semester.diaryId == current.diaryId } - view?.setCurrentSemesterName(current.semesterName, schoolYear) - - view?.run { - Timber.i("Loading grade result: Attempt load index $currentPageIndex") - loadChild(currentPageIndex) - showErrorView(false) - showSemesterSwitch(true) - } - } - Status.ERROR -> { - Timber.i("Loading grade result: An exception occurred") - errorHandler.dispatch(it.error!!) + } + .logResourceStatus("load grade data") + .onResourceData { + val current = it.getCurrentOrLast() + selectedIndex = if (selectedIndex == 0) current.semesterName else selectedIndex + schoolYear = current.schoolYear + semesters = it.filter { semester -> semester.diaryId == current.diaryId } + view?.setCurrentSemesterName(current.semesterName, schoolYear) + view?.run { + Timber.i("Loading grade data: Attempt load index $currentPageIndex") + loadChild(currentPageIndex) + showErrorView(false) + showSemesterSwitch(true) } } - }.launch() + .onResourceError(errorHandler::dispatch) + .launch() } private fun showErrorViewOnError(message: String, error: Throwable) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsDialog.kt index a9d9039dd..34594111f 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsDialog.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsDialog.kt @@ -10,11 +10,8 @@ import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Grade import io.github.wulkanowy.data.enums.GradeColorTheme import io.github.wulkanowy.databinding.DialogGradeBinding -import io.github.wulkanowy.utils.colorStringId -import io.github.wulkanowy.utils.getBackgroundColor -import io.github.wulkanowy.utils.getGradeColor -import io.github.wulkanowy.utils.lifecycleAwareVariable -import io.github.wulkanowy.utils.toFormattedString +import io.github.wulkanowy.utils.* + class GradeDetailsDialog : DialogFragment() { @@ -80,9 +77,7 @@ class GradeDetailsDialog : DialogFragment() { setBackgroundResource(grade.getBackgroundColor(gradeColorTheme)) } - gradeDialogTeacherValue.text = if (grade.teacher.isBlank()) { - getString(R.string.all_no_data) - } else grade.teacher + gradeDialogTeacherValue.text = grade.teacher.ifBlank { getString(R.string.all_no_data) } gradeDialogDescriptionValue.text = grade.run { when { 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 9141e0438..4261c507d 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsPresenter.kt @@ -1,10 +1,9 @@ package io.github.wulkanowy.ui.modules.grade.details -import io.github.wulkanowy.data.Status +import io.github.wulkanowy.data.* import io.github.wulkanowy.data.db.entities.Grade import io.github.wulkanowy.data.enums.GradeExpandMode -import io.github.wulkanowy.data.enums.GradeSortingMode.ALPHABETIC -import io.github.wulkanowy.data.enums.GradeSortingMode.DATE +import io.github.wulkanowy.data.enums.GradeSortingMode.* import io.github.wulkanowy.data.repositories.GradeRepository import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.SemesterRepository @@ -14,12 +13,8 @@ import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.ui.modules.grade.GradeAverageProvider import io.github.wulkanowy.ui.modules.grade.GradeSubject 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 import javax.inject.Inject @@ -71,7 +66,7 @@ class GradeDetailsPresenter @Inject constructor( } fun onMarkAsReadSelected(): Boolean { - flowWithResource { + resourceFlow { val student = studentRepository.getCurrentStudent() val semesters = semesterRepository.getSemesters(student) val semester = semesters.first { item -> item.semesterId == currentSemesterId } @@ -79,19 +74,11 @@ class GradeDetailsPresenter @Inject constructor( Timber.i("Mark as read ${unreadGrades.size} grades") gradeRepository.updateGrades(unreadGrades.map { it.apply { isRead = true } }) - }.onEach { - when (it.status) { - Status.LOADING -> Timber.i("Select mark grades as read") - Status.SUCCESS -> { - Timber.i("Mark as read result: Success") - loadData(currentSemesterId, false) - } - Status.ERROR -> { - Timber.i("Mark as read result: An exception occurred") - errorHandler.dispatch(it.error!!) - } - } - }.launch("mark") + } + .logResourceStatus("mark grades as read") + .onResourceSuccess { loadData(currentSemesterId, false) } + .onResourceError(errorHandler::dispatch) + .launch("mark") return true } @@ -138,71 +125,50 @@ class GradeDetailsPresenter @Inject constructor( } private fun loadData(semesterId: Int, forceRefresh: Boolean) { - Timber.i("Loading grade details data started") - - flowWithResourceIn { + flatResourceFlow { val student = studentRepository.getCurrentStudent() averageProvider.getGradesDetailsWithAverage(student, semesterId, forceRefresh) - }.onEach { - Timber.d("Loading grade details status: ${it.status}, data: ${it.data != null}") - when (it.status) { - Status.LOADING -> { - val items = createGradeItems(it.data.orEmpty()) - if (items.isNotEmpty()) { - Timber.i("Loading grade details result: load cached data") - view?.run { - updateNewGradesAmount(it.data.orEmpty()) - enableSwipe(true) - showRefresh(true) - showProgress(false) - showEmpty(false) - showContent(true) - updateData( - data = items, - expandMode = preferencesRepository.gradeExpandMode, - gradeColorTheme = preferencesRepository.gradeColorTheme - ) - notifyParentDataLoaded(semesterId) - } - } - } - Status.SUCCESS -> { - Timber.i("Loading grade details result: Success") - updateNewGradesAmount(it.data!!) + } + .logResourceStatus("load grade details") + .onResourceData { + val gradeItems = createGradeItems(it) + view?.run { + enableSwipe(true) + showProgress(false) + showErrorView(false) + showContent(gradeItems.isNotEmpty()) + showEmpty(gradeItems.isEmpty()) + updateNewGradesAmount(it) updateMarkAsDoneButton() - val items = createGradeItems(it.data) - view?.run { - showEmpty(items.isEmpty()) - showErrorView(false) - showContent(items.isNotEmpty()) - updateData( - data = items, - expandMode = preferencesRepository.gradeExpandMode, - gradeColorTheme = preferencesRepository.gradeColorTheme - ) - } - analytics.logEvent( - "load_data", - "type" to "grade_details", - "items" to it.data.size + updateData( + data = gradeItems, + expandMode = preferencesRepository.gradeExpandMode, + preferencesRepository.gradeColorTheme ) } - Status.ERROR -> { - Timber.i("Loading grade details result: An exception occurred") - errorHandler.dispatch(it.error!!) + } + .onResourceIntermediate { view?.showRefresh(true) } + .onResourceSuccess { + analytics.logEvent( + "load_data", + "type" to "grade_details", + "items" to it.size + ) + } + .onResourceNotLoading { + view?.run { + enableSwipe(true) + showRefresh(false) + showProgress(false) + notifyParentDataLoaded(semesterId) } } - }.afterLoading { - view?.run { - showRefresh(false) - showProgress(false) - enableSwipe(true) - notifyParentDataLoaded(semesterId) + .catch { + errorHandler.dispatch(it) + view?.notifyParentDataLoaded(semesterId) } - }.catch { - errorHandler.dispatch(it) - view?.notifyParentDataLoaded(semesterId) - }.launch() + .onResourceError(errorHandler::dispatch) + .launch() } private fun updateNewGradesAmount(grades: List) { @@ -238,6 +204,7 @@ class GradeDetailsPresenter @Inject constructor( ALPHABETIC -> gradeSubjects.sortedBy { gradeDetailsWithAverage -> gradeDetailsWithAverage.subject.lowercase() } + AVERAGE -> gradeSubjects.sortedByDescending { it.average } } } .map { (subject, average, points, _, grades) -> @@ -267,15 +234,9 @@ class GradeDetailsPresenter @Inject constructor( } private fun updateGrade(grade: Grade) { - flowWithResource { gradeRepository.updateGrade(grade) }.onEach { - when (it.status) { - Status.LOADING -> Timber.i("Attempt to update grade ${grade.id}") - Status.SUCCESS -> Timber.i("Update grade result: Success") - Status.ERROR -> { - Timber.i("Update grade result: An exception occurred") - errorHandler.dispatch(it.error!!) - } - } - }.launch("update") + resourceFlow { gradeRepository.updateGrade(grade) } + .logResourceStatus("update grade result ${grade.id}") + .onResourceError(errorHandler::dispatch) + .launch("update") } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/statistics/GradeStatisticsAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/statistics/GradeStatisticsAdapter.kt index 6be2d969f..fd0ac5471 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/statistics/GradeStatisticsAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/statistics/GradeStatisticsAdapter.kt @@ -9,12 +9,7 @@ import androidx.core.content.ContextCompat import androidx.recyclerview.widget.RecyclerView import com.github.mikephil.charting.components.Legend import com.github.mikephil.charting.components.LegendEntry -import com.github.mikephil.charting.data.BarData -import com.github.mikephil.charting.data.BarDataSet -import com.github.mikephil.charting.data.BarEntry -import com.github.mikephil.charting.data.PieData -import com.github.mikephil.charting.data.PieDataSet -import com.github.mikephil.charting.data.PieEntry +import com.github.mikephil.charting.data.* import com.github.mikephil.charting.formatter.ValueFormatter import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.GradePartialStatistics @@ -136,20 +131,50 @@ class GradeStatisticsAdapter @Inject constructor() : binding: ItemGradeStatisticsPieBinding, partials: GradePartialStatistics ) { - bindPieChart(binding, partials.subject, partials.classAverage, partials.classAmounts) + val studentAverage = partials.studentAverage.takeIf { it.isNotEmpty() }?.let { + binding.root.context.getString(R.string.grade_statistics_student_average, it) + } + bindPieChart( + binding = binding, + subject = partials.subject, + average = partials.classAverage, + studentValue = studentAverage, + amounts = partials.classAmounts + ) } private fun bindSemesterChart( binding: ItemGradeStatisticsPieBinding, semester: GradeSemesterStatistics ) { - bindPieChart(binding, semester.subject, semester.average, semester.amounts) + val studentAverage = semester.studentAverage.takeIf { it.isNotBlank() } + val studentGrade = semester.studentGrade.takeIf { it != 0 } + + val studentValue = when { + studentAverage != null -> binding.root.context.getString( + R.string.grade_statistics_student_average, + studentAverage + ) + studentGrade != null -> binding.root.context.getString( + R.string.grade_statistics_student_grade, + studentGrade.toString() + ) + else -> null + } + bindPieChart( + binding = binding, + subject = semester.subject, + average = semester.classAverage, + studentValue = studentValue, + amounts = semester.amounts + ) } private fun bindPieChart( binding: ItemGradeStatisticsPieBinding, subject: String, average: String, + studentValue: String?, amounts: List ) { with(binding.gradeStatisticsPieTitle) { @@ -208,13 +233,13 @@ class GradeStatisticsAdapter @Inject constructor() : val numberOfGradesString = amounts.fold(0) { acc, it -> acc + it } .let { resources.getQuantityString(R.plurals.grade_number_item, it, it) } val averageString = - binding.root.context.getString(R.string.grade_statistics_average, average) + binding.root.context.getString(R.string.grade_statistics_class_average, average) minAngleForSlices = 25f description.isEnabled = false centerText = numberOfGradesString + ("\n\n" + averageString).takeIf { average.isNotBlank() } - .orEmpty() + .orEmpty() + studentValue?.let { "\n$it" }.orEmpty() setHoleColor(context.getThemeAttrColor(android.R.attr.windowBackground)) setCenterTextColor(context.getThemeAttrColor(android.R.attr.textColorPrimary)) 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 35d007749..2af59c011 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 @@ -33,6 +33,7 @@ class GradeStatisticsFragment : companion object { private const val SAVED_CHART_TYPE = "CURRENT_TYPE" + private const val SAVED_SUBJECT_NAME = "SUBJECT_NAME" fun newInstance() = GradeStatisticsFragment() } @@ -46,8 +47,9 @@ class GradeStatisticsFragment : binding = FragmentGradeStatisticsBinding.bind(view) messageContainer = binding.gradeStatisticsRecycler presenter.onAttachView( - this, - savedInstanceState?.getSerializable(SAVED_CHART_TYPE) as? GradeStatisticsItem.DataType + view = this, + type = savedInstanceState?.getSerializable(SAVED_CHART_TYPE) as? GradeStatisticsItem.DataType, + subjectName = savedInstanceState?.getSerializable(SAVED_SUBJECT_NAME) as? String, ) } @@ -56,6 +58,7 @@ class GradeStatisticsFragment : with(binding.gradeStatisticsRecycler) { layoutManager = LinearLayoutManager(requireContext()) + statisticsAdapter.currentDataType = presenter.currentType adapter = statisticsAdapter } @@ -81,7 +84,8 @@ class GradeStatisticsFragment : } } - override fun updateSubjects(data: ArrayList) { + override fun updateSubjects(data: List, selectedIndex: Int) { + binding.gradeStatisticsSubjects.setSelection(selectedIndex) with(subjectsAdapter) { clear() addAll(data) @@ -161,6 +165,7 @@ class GradeStatisticsFragment : override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) outState.putSerializable(SAVED_CHART_TYPE, presenter.currentType) + outState.putSerializable(SAVED_SUBJECT_NAME, presenter.currentSubjectName) } override fun onDestroyView() { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/statistics/GradeStatisticsPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/statistics/GradeStatisticsPresenter.kt index 53eccad65..aa0e5999e 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/statistics/GradeStatisticsPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/statistics/GradeStatisticsPresenter.kt @@ -1,19 +1,12 @@ package io.github.wulkanowy.ui.modules.grade.statistics -import io.github.wulkanowy.data.Status +import io.github.wulkanowy.data.* import io.github.wulkanowy.data.db.entities.Subject import io.github.wulkanowy.data.pojos.GradeStatisticsItem -import io.github.wulkanowy.data.repositories.GradeStatisticsRepository -import io.github.wulkanowy.data.repositories.PreferencesRepository -import io.github.wulkanowy.data.repositories.SemesterRepository -import io.github.wulkanowy.data.repositories.StudentRepository -import io.github.wulkanowy.data.repositories.SubjectRepository +import io.github.wulkanowy.data.repositories.* 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.afterLoading -import io.github.wulkanowy.utils.flowWithResourceIn -import kotlinx.coroutines.flow.onEach import timber.log.Timber import javax.inject.Inject @@ -31,16 +24,22 @@ class GradeStatisticsPresenter @Inject constructor( private var currentSemesterId = 0 - private var currentSubjectName: String = "Wszystkie" + var currentSubjectName: String = "Wszystkie" + private set private lateinit var lastError: Throwable var currentType: GradeStatisticsItem.DataType = GradeStatisticsItem.DataType.PARTIAL private set - fun onAttachView(view: GradeStatisticsView, type: GradeStatisticsItem.DataType?) { + fun onAttachView( + view: GradeStatisticsView, + type: GradeStatisticsItem.DataType?, + subjectName: String? + ) { super.onAttachView(view) currentType = type ?: GradeStatisticsItem.DataType.PARTIAL + currentSubjectName = subjectName ?: currentSubjectName view.initView() errorHandler.showErrorMessage = ::showErrorViewOnError } @@ -119,28 +118,26 @@ class GradeStatisticsPresenter @Inject constructor( } private fun loadSubjects() { - flowWithResourceIn { + flatResourceFlow { val student = studentRepository.getCurrentStudent() val semester = semesterRepository.getCurrentSemester(student) subjectRepository.getSubjects(student, semester) - }.onEach { - when (it.status) { - Status.LOADING -> Timber.i("Loading grade stats subjects started") - Status.SUCCESS -> { - subjects = it.data!! - - Timber.i("Loading grade stats subjects result: Success") - view?.run { - view?.updateSubjects(ArrayList(it.data.map { subject -> subject.name })) - showSubjects(!preferencesRepository.showAllSubjectsOnStatisticsList) - } - } - Status.ERROR -> { - Timber.i("Loading grade stats subjects result: An exception occurred") - errorHandler.dispatch(it.error!!) + } + .logResourceStatus("load grade stats subjects") + .onResourceData { + subjects = it + view?.run { + showSubjects(!preferencesRepository.showAllSubjectsOnStatisticsList) + updateSubjects( + data = it.map { subject -> subject.name }, + selectedIndex = it.indexOfFirst { subject -> + subject.name == currentSubjectName + }, + ) } } - }.launch("subjects") + .onResourceError(errorHandler::dispatch) + .launch("subjects") } private fun loadDataByType( @@ -151,11 +148,13 @@ class GradeStatisticsPresenter @Inject constructor( ) { Timber.i("Loading grade stats data started") - currentSubjectName = - if (preferencesRepository.showAllSubjectsOnStatisticsList) "Wszystkie" else subjectName currentType = type + currentSubjectName = when { + preferencesRepository.showAllSubjectsOnStatisticsList -> "Wszystkie" + else -> subjectName + } - flowWithResourceIn { + flatResourceFlow { val student = studentRepository.getCurrentStudent() val semesters = semesterRepository.getSemesters(student) val semester = semesters.first { item -> item.semesterId == semesterId } @@ -188,58 +187,43 @@ class GradeStatisticsPresenter @Inject constructor( } } } - }.onEach { - when (it.status) { - Status.LOADING -> { - val isNoContent = it.data == null || checkIsNoContent(it.data, type) - if (!isNoContent) { - view?.run { - showEmpty(isNoContent) - showErrorView(false) - enableSwipe(true) - showRefresh(true) - showProgress(false) - updateData( - if (isNoContent) emptyList() else it.data!!, - preferencesRepository.gradeColorTheme, - preferencesRepository.showAllSubjectsOnStatisticsList - ) - showSubjects(!preferencesRepository.showAllSubjectsOnStatisticsList) - } - } - } - Status.SUCCESS -> { - Timber.i("Loading grade stats result: Success") - view?.run { - val isNoContent = checkIsNoContent(it.data!!, type) - showEmpty(isNoContent) - showErrorView(false) - updateData( - if (isNoContent) emptyList() else it.data, - preferencesRepository.gradeColorTheme, - preferencesRepository.showAllSubjectsOnStatisticsList - ) - showSubjects(!preferencesRepository.showAllSubjectsOnStatisticsList) - } - analytics.logEvent( - "load_data", - "type" to "grade_statistics", - "items" to it.data!!.size + } + .logResourceStatus("load grade stats data") + .mapResourceData { + val isNoContent = checkIsNoContent(it, type) + if (isNoContent) emptyList() else it + } + .onResourceData { + view?.run { + enableSwipe(true) + showProgress(false) + showErrorView(false) + showEmpty(it.isEmpty()) + updateData( + newItems = it, + newTheme = preferencesRepository.gradeColorTheme, + showAllSubjectsOnStatisticsList = preferencesRepository.showAllSubjectsOnStatisticsList ) } - Status.ERROR -> { - Timber.i("Loading grade stats result: An exception occurred") - errorHandler.dispatch(it.error!!) + } + .onResourceIntermediate { view?.showRefresh(true) } + .onResourceSuccess { + analytics.logEvent( + "load_data", + "type" to "grade_statistics", + "items" to it.size + ) + } + .onResourceNotLoading { + view?.run { + enableSwipe(true) + showRefresh(false) + showProgress(false) + notifyParentDataLoaded(semesterId) } } - }.afterLoading { - view?.run { - showRefresh(false) - showProgress(false) - enableSwipe(true) - notifyParentDataLoaded(semesterId) - } - }.launch("load") + .onResourceError(errorHandler::dispatch) + .launch("load") } private fun checkIsNoContent( @@ -254,7 +238,8 @@ class GradeStatisticsPresenter @Inject constructor( items.firstOrNull()?.partial?.classAmounts.orEmpty().sum() == 0 } GradeStatisticsItem.DataType.POINTS -> { - items.firstOrNull()?.points?.let { points -> points.student == .0 && points.others == .0 } ?: false + items.firstOrNull()?.points?.let { points -> points.student == .0 && points.others == .0 } + ?: false } } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/statistics/GradeStatisticsView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/statistics/GradeStatisticsView.kt index 8e9a206bc..4333bb0a9 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/statistics/GradeStatisticsView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/statistics/GradeStatisticsView.kt @@ -12,7 +12,7 @@ interface GradeStatisticsView : BaseView { fun initView() - fun updateSubjects(data: ArrayList) + fun updateSubjects(data: List, selectedIndex: Int) fun updateData( newItems: List, 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 933633dca..4d5a43d8f 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryPresenter.kt @@ -1,22 +1,23 @@ package io.github.wulkanowy.ui.modules.grade.summary -import io.github.wulkanowy.data.Status +import io.github.wulkanowy.data.* import io.github.wulkanowy.data.db.entities.GradeSummary +import io.github.wulkanowy.data.enums.GradeSortingMode +import io.github.wulkanowy.data.enums.GradeSortingMode.* +import io.github.wulkanowy.data.repositories.PreferencesRepository 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.GradeSubject import io.github.wulkanowy.utils.AnalyticsHelper -import io.github.wulkanowy.utils.afterLoading -import io.github.wulkanowy.utils.flowWithResourceIn -import kotlinx.coroutines.flow.onEach import timber.log.Timber import javax.inject.Inject class GradeSummaryPresenter @Inject constructor( errorHandler: ErrorHandler, studentRepository: StudentRepository, + private val preferencesRepository: PreferencesRepository, private val averageProvider: GradeAverageProvider, private val analytics: AnalyticsHelper ) : BasePresenter(errorHandler, studentRepository) { @@ -37,56 +38,40 @@ class GradeSummaryPresenter @Inject constructor( } private fun loadData(semesterId: Int, forceRefresh: Boolean) { - Timber.i("Loading grade summary started") - - flowWithResourceIn { + flatResourceFlow { val student = studentRepository.getCurrentStudent() averageProvider.getGradesDetailsWithAverage(student, semesterId, forceRefresh) - }.onEach { - Timber.d("Loading grade summary status: ${it.status}, data: ${it.data != null}") - when (it.status) { - Status.LOADING -> { - val items = createGradeSummaryItems(it.data.orEmpty()) - if (items.isNotEmpty()) { - Timber.i("Loading grade summary result: load cached data") - view?.run { - enableSwipe(true) - showRefresh(true) - showProgress(false) - showEmpty(false) - showContent(true) - updateData(items) - } - } - } - Status.SUCCESS -> { - Timber.i("Loading grade summary result: Success") - val items = createGradeSummaryItems(it.data!!) - view?.run { - showEmpty(items.isEmpty()) - showContent(items.isNotEmpty()) - showErrorView(false) - updateData(items) - } - analytics.logEvent( - "load_data", - "type" to "grade_summary", - "items" to it.data.size - ) - } - Status.ERROR -> { - Timber.i("Loading grade summary result: An exception occurred") - errorHandler.dispatch(it.error!!) + } + .logResourceStatus("load grade summary", showData = true) + .mapResourceData { createGradeSummaryItems(it) } + .onResourceData { + view?.run { + enableSwipe(true) + showProgress(false) + showErrorView(false) + showContent(it.isNotEmpty()) + showEmpty(it.isEmpty()) + updateData(it) } } - }.afterLoading { - view?.run { - showRefresh(false) - showProgress(false) - enableSwipe(true) - notifyParentDataLoaded(semesterId) + .onResourceIntermediate { view?.showRefresh(true) } + .onResourceSuccess { + analytics.logEvent( + "load_data", + "type" to "grade_summary", + "items" to it.size + ) } - }.launch() + .onResourceNotLoading { + view?.run { + enableSwipe(true) + showRefresh(false) + showProgress(false) + notifyParentDataLoaded(semesterId) + } + } + .onResourceError(errorHandler::dispatch) + .launch() } private fun showErrorViewOnError(message: String, error: Throwable) { @@ -146,16 +131,26 @@ class GradeSummaryPresenter @Inject constructor( private fun createGradeSummaryItems(items: List): List { return items .filter { !checkEmpty(it) } - .sortedBy { it.subject } + .let { gradeSubjects -> + when (preferencesRepository.gradeSortingMode) { + DATE -> gradeSubjects.sortedByDescending { gradeDetailsWithAverage -> + gradeDetailsWithAverage.grades.maxByOrNull { it.date }?.date + } + ALPHABETIC -> gradeSubjects.sortedBy { gradeDetailsWithAverage -> + gradeDetailsWithAverage.subject.lowercase() + } + AVERAGE -> gradeSubjects.sortedByDescending { it.average } + } + } .map { it.summary.copy(average = it.average) } } private fun checkEmpty(gradeSummary: GradeSubject): Boolean { return gradeSummary.run { summary.finalGrade.isBlank() - && summary.predictedGrade.isBlank() - && average == .0 - && points.isBlank() + && summary.predictedGrade.isBlank() + && average == .0 + && points.isBlank() } } } 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 d7d5d7cb9..2ac552b41 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 @@ -1,21 +1,13 @@ package io.github.wulkanowy.ui.modules.homework -import io.github.wulkanowy.data.Status +import io.github.wulkanowy.data.* 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.AnalyticsHelper -import io.github.wulkanowy.utils.afterLoading -import io.github.wulkanowy.utils.flowWithResourceIn -import io.github.wulkanowy.utils.getLastSchoolDayIfHoliday -import io.github.wulkanowy.utils.isHolidays -import io.github.wulkanowy.utils.monday -import io.github.wulkanowy.utils.nextOrSameSchoolDay -import io.github.wulkanowy.utils.sunday -import io.github.wulkanowy.utils.toFormattedString +import io.github.wulkanowy.utils.* import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.onEach @@ -89,61 +81,59 @@ class HomeworkPresenter @Inject constructor( flow { val student = studentRepository.getCurrentStudent() emit(semesterRepository.getCurrentSemester(student)) - }.catch { - Timber.i("Loading semester result: An exception occurred") - }.onEach { - baseDate = baseDate.getLastSchoolDayIfHoliday(it.schoolYear) - currentDate = baseDate - reloadNavigation() - }.launch("holidays") + } + .catch { Timber.i("Loading semester result: An exception occurred") } + .onEach { + baseDate = baseDate.getLastSchoolDayIfHoliday(it.schoolYear) + currentDate = baseDate + reloadNavigation() + } + .launch("holidays") } private fun loadData(forceRefresh: Boolean = false) { Timber.i("Loading homework data started") - flowWithResourceIn { + flatResourceFlow { val student = studentRepository.getCurrentStudent() val semester = semesterRepository.getCurrentSemester(student) - homeworkRepository.getHomework(student, semester, currentDate, currentDate, forceRefresh) - }.onEach { - when (it.status) { - Status.LOADING -> { - if (!it.data.isNullOrEmpty()) { - view?.run { - enableSwipe(true) - showRefresh(true) - showProgress(false) - showContent(true) - updateData(createHomeworkItem(it.data)) - } - } - } - Status.SUCCESS -> { - Timber.i("Loading homework result: Success") - view?.apply { - updateData(createHomeworkItem(it.data!!)) - showEmpty(it.data.isEmpty()) - showErrorView(false) - showContent(it.data.isNotEmpty()) - } - analytics.logEvent( - "load_data", - "type" to "homework", - "items" to it.data!!.size - ) - } - Status.ERROR -> { - Timber.i("Loading homework result: An exception occurred") - errorHandler.dispatch(it.error!!) + homeworkRepository.getHomework( + student = student, + semester = semester, + start = currentDate, + end = currentDate, + forceRefresh = forceRefresh + ) + } + .logResourceStatus("loading homework") + .mapResourceData { createHomeworkItem(it) } + .onResourceData { + view?.run { + enableSwipe(true) + showProgress(false) + showErrorView(false) + showContent(it.isNotEmpty()) + showEmpty(it.isEmpty()) + updateData(it) } } - }.afterLoading { - view?.run { - showRefresh(false) - showProgress(false) - enableSwipe(true) + .onResourceIntermediate { view?.showRefresh(true) } + .onResourceSuccess { + analytics.logEvent( + "load_data", + "type" to "homework", + "items" to it.size + ) } - }.launch() + .onResourceNotLoading { + view?.run { + enableSwipe(true) + showProgress(false) + showRefresh(false) + } + } + .onResourceError(errorHandler::dispatch) + .launch() } private fun showErrorViewOnError(message: String, error: Throwable) { @@ -159,9 +149,10 @@ class HomeworkPresenter @Inject constructor( private fun createHomeworkItem(items: List): List> { return items.groupBy { it.date }.toSortedMap().map { (date, exams) -> - listOf(HomeworkItem(date, HomeworkItem.ViewType.HEADER)) + exams.reversed().map { exam -> - HomeworkItem(exam, HomeworkItem.ViewType.ITEM) - } + listOf(HomeworkItem(date, HomeworkItem.ViewType.HEADER)) + exams.reversed() + .map { exam -> + HomeworkItem(exam, HomeworkItem.ViewType.ITEM) + } }.flatten() } @@ -184,8 +175,10 @@ class HomeworkPresenter @Inject constructor( view?.apply { showPreButton(!currentDate.minusDays(7).isHolidays) showNextButton(!currentDate.plusDays(7).isHolidays) - updateNavigationWeek("${currentDate.monday.toFormattedString("dd.MM")} - " + - currentDate.sunday.toFormattedString("dd.MM")) + updateNavigationWeek( + "${currentDate.monday.toFormattedString("dd.MM")} - " + + currentDate.sunday.toFormattedString("dd.MM") + ) } } } 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 index 12168f142..c2aff2b13 100644 --- 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 @@ -5,15 +5,13 @@ 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.lastSchoolDayInSchoolYear +import io.github.wulkanowy.utils.openMaterialDatePicker 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 @@ -23,6 +21,7 @@ class HomeworkAddDialog : BaseDialogFragment(), Homewo @Inject lateinit var presenter: HomeworkAddPresenter + // todo: move it to presenter private var date: LocalDate? = null override fun onCreate(savedInstanceState: Bundle?) { @@ -97,24 +96,16 @@ class HomeworkAddDialog : BaseDialogFragment(), Homewo 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 showDatePickerDialog(selectedDate: LocalDate) { + openMaterialDatePicker( + selected = selectedDate, + rangeStart = LocalDate.now(), + rangeEnd = LocalDate.now().lastSchoolDayInSchoolYear, + onDateSelected = { + date = it + binding.homeworkDialogDate.editText?.setText(date!!.toFormattedString()) + } + ) } override fun 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 index 3639c2fef..a21f6aef7 100644 --- 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 @@ -1,15 +1,16 @@ 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.logResourceStatus +import io.github.wulkanowy.data.onResourceError +import io.github.wulkanowy.data.onResourceSuccess 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.data.resourceFlow 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 @@ -55,7 +56,7 @@ class HomeworkAddPresenter @Inject constructor( } private fun saveHomework(subject: String, teacher: String, date: LocalDate, content: String) { - flowWithResource { + resourceFlow { val student = studentRepository.getCurrentStudent() val semester = semesterRepository.getCurrentSemester(student) val entryDate = LocalDate.now() @@ -72,21 +73,15 @@ class HomeworkAddPresenter @Inject constructor( 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!!) + } + .logResourceStatus("homework insert") + .onResourceSuccess { + view?.run { + showSuccessMessage() + closeDialog() } } - }.launch("add_homework") + .onResourceError(errorHandler::dispatch) + .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 index 3bb304d9c..91414ae2f 100644 --- 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 @@ -17,5 +17,5 @@ interface HomeworkAddView : BaseView { fun closeDialog() - fun showDatePickerDialog(currentDate: LocalDate) + fun showDatePickerDialog(selectedDate: LocalDate) } 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 ea9b47a05..e76df6bd0 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 @@ -1,15 +1,16 @@ package io.github.wulkanowy.ui.modules.homework.details -import io.github.wulkanowy.data.Status import io.github.wulkanowy.data.db.entities.Homework +import io.github.wulkanowy.data.logResourceStatus +import io.github.wulkanowy.data.onResourceError +import io.github.wulkanowy.data.onResourceSuccess import io.github.wulkanowy.data.repositories.HomeworkRepository import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.StudentRepository +import io.github.wulkanowy.data.resourceFlow import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.utils.AnalyticsHelper -import io.github.wulkanowy.utils.flowWithResource -import kotlinx.coroutines.flow.onEach import timber.log.Timber import javax.inject.Inject @@ -34,38 +35,26 @@ class HomeworkDetailsPresenter @Inject constructor( } 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!!) + resourceFlow { homeworkRepository.deleteHomework(homework) } + .logResourceStatus("homework delete") + .onResourceSuccess { + view?.run { + showMessage(homeworkDeleteSuccess) + closeDialog() } } - }.launch("delete") + .onResourceError(errorHandler::dispatch) + .launch("delete") } fun toggleDone(homework: Homework) { - flowWithResource { homeworkRepository.toggleDone(homework) }.onEach { - when (it.status) { - Status.LOADING -> Timber.i("Homework details update start") - Status.SUCCESS -> { - Timber.i("Homework details update: Success") - view?.updateMarkAsDoneLabel(homework.isDone) - analytics.logEvent("homework_mark_as_done") - } - Status.ERROR -> { - Timber.i("Homework details update result: An exception occurred") - errorHandler.dispatch(it.error!!) - } + resourceFlow { homeworkRepository.toggleDone(homework) } + .logResourceStatus("homework details update") + .onResourceSuccess { + view?.updateMarkAsDoneLabel(homework.isDone) + analytics.logEvent("homework_mark_as_done") } - }.launch("toggle") + .onResourceError(errorHandler::dispatch) + .launch("toggle") } } 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 e607cef15..aac60b56d 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 @@ -4,18 +4,19 @@ import android.content.Context import android.content.Intent import android.os.Bundle import android.view.MenuItem +import androidx.fragment.app.Fragment +import androidx.fragment.app.commit import dagger.hilt.android.AndroidEntryPoint +import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.databinding.ActivityLoginBinding import io.github.wulkanowy.ui.base.BaseActivity -import io.github.wulkanowy.ui.base.BaseFragmentPagerAdapter import io.github.wulkanowy.ui.modules.login.advanced.LoginAdvancedFragment import io.github.wulkanowy.ui.modules.login.form.LoginFormFragment import io.github.wulkanowy.ui.modules.login.recover.LoginRecoverFragment import io.github.wulkanowy.ui.modules.login.studentselect.LoginStudentSelectFragment import io.github.wulkanowy.ui.modules.login.symbol.LoginSymbolFragment import io.github.wulkanowy.utils.UpdateHelper -import io.github.wulkanowy.utils.setOnSelectPageListener import javax.inject.Inject @AndroidEntryPoint @@ -24,21 +25,10 @@ class LoginActivity : BaseActivity(), Logi @Inject override lateinit var presenter: LoginPresenter - 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) } @@ -51,18 +41,10 @@ class LoginActivity : BaseActivity(), Logi presenter.onAttachView(this) updateHelper.checkAndInstallUpdates(this) - } - override fun onResume() { - super.onResume() - updateHelper.onResume(this) - } - - //https://developer.android.com/guide/playcore/in-app-updates#status_callback - @Suppress("DEPRECATION") - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - updateHelper.onActivityResult(requestCode, resultCode) + if (savedInstanceState == null) { + openFragment(LoginFormFragment.newInstance(), clearBackStack = true) + } } override fun initView() { @@ -70,27 +52,6 @@ class LoginActivity : BaseActivity(), Logi setDisplayHomeAsUpEnabled(true) setDisplayShowTitleEnabled(false) } - - with(binding.loginViewpager) { - adapter = pagerAdapter - isUserInputEnabled = false - offscreenPageLimit = 2 - 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 { @@ -98,44 +59,44 @@ class LoginActivity : BaseActivity(), Logi return true } - override fun switchView(index: Int) { - binding.loginViewpager.setCurrentItem(index, false) - } - - override fun showActionBar(show: Boolean) { + fun showActionBar(show: Boolean) { supportActionBar?.run { if (show) show() else hide() } } - override fun onBackPressed() { - presenter.onBackPressed { super.onBackPressed() } + fun navigateToSymbolFragment(loginData: LoginData) { + openFragment(LoginSymbolFragment.newInstance(loginData)) } - override fun notifyInitSymbolFragment(loginData: Triple) { - (pagerAdapter.getFragmentInstance(1) as? LoginSymbolFragment) - ?.onParentInitSymbolFragment(loginData) - } - - override fun notifyInitStudentSelectFragment(studentsWithSemesters: List) { - (pagerAdapter.getFragmentInstance(2) as? LoginStudentSelectFragment) - ?.onParentInitStudentSelectFragment(studentsWithSemesters) - } - - fun onFormFragmentAccountLogged( - studentsWithSemesters: List, - loginData: Triple - ) { - presenter.onFormViewAccountLogged(studentsWithSemesters, loginData) - } - - fun onSymbolFragmentAccountLogged(studentsWithSemesters: List) { - presenter.onSymbolViewAccountLogged(studentsWithSemesters) + fun navigateToStudentSelect(studentsWithSemesters: List) { + openFragment(LoginStudentSelectFragment.newInstance(studentsWithSemesters)) } fun onAdvancedLoginClick() { - presenter.onAdvancedLoginClick() + openFragment(LoginAdvancedFragment.newInstance()) } fun onRecoverClick() { - presenter.onRecoverClick() + openFragment(LoginRecoverFragment.newInstance()) + } + + private fun openFragment(fragment: Fragment, clearBackStack: Boolean = false) { + supportFragmentManager.commit { + replace(R.id.loginContainer, fragment) + setReorderingAllowed(true) + if (!clearBackStack) addToBackStack(fragment::class.java.name) + } + } + + override fun onResume() { + super.onResume() + updateHelper.onResume(this) + } + + //https://developer.android.com/guide/playcore/in-app-updates#status_callback + @Deprecated("Deprecated in Java") + @Suppress("DEPRECATION") + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + updateHelper.onActivityResult(requestCode, resultCode) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginData.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginData.kt new file mode 100644 index 000000000..5d4743589 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginData.kt @@ -0,0 +1,9 @@ +package io.github.wulkanowy.ui.modules.login + +import java.io.Serializable + +data class LoginData( + val login: String, + val password: String, + val baseUrl: String, +) : Serializable 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 ea7215cea..37ab71dce 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 @@ -12,8 +12,9 @@ import io.github.wulkanowy.sdk.scrapper.login.BadCredentialsException import io.github.wulkanowy.ui.base.ErrorHandler import javax.inject.Inject -class LoginErrorHandler @Inject constructor(@ApplicationContext context: Context) : - ErrorHandler(context) { +class LoginErrorHandler @Inject constructor( + @ApplicationContext context: Context, +) : ErrorHandler(context) { var onBadCredentials: (String?) -> Unit = {} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginPresenter.kt index aa1e7eced..9031cb8ab 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginPresenter.kt @@ -1,6 +1,5 @@ package io.github.wulkanowy.ui.modules.login -import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.ErrorHandler @@ -14,59 +13,7 @@ class LoginPresenter @Inject constructor( override fun onAttachView(view: LoginView) { super.onAttachView(view) - with(view) { - initView() - showActionBar(false) - } + view.initView() Timber.i("Login view was initialized") } - - fun onFormViewAccountLogged(studentsWithSemesters: List, loginData: Triple) { - view?.apply { - if (studentsWithSemesters.isEmpty()) { - Timber.i("Switch to symbol form") - notifyInitSymbolFragment(loginData) - switchView(1) - } else { - Timber.i("Switch to student select") - notifyInitStudentSelectFragment(studentsWithSemesters) - switchView(2) - } - } - } - - fun onSymbolViewAccountLogged(studentsWithSemesters: List) { - view?.apply { - Timber.i("Switch to student select") - notifyInitStudentSelectFragment(studentsWithSemesters) - switchView(2) - } - } - - fun onAdvancedLoginClick() { - view?.switchView(3) - } - - fun onRecoverClick() { - view?.switchView(4) - } - - fun onViewSelected(index: Int) { - view?.apply { - when (index) { - 0 -> showActionBar(false) - 1, 2, 3, 4 -> showActionBar(true) - } - } - } - - fun onBackPressed(default: () -> Unit) { - Timber.i("Back pressed in login view") - view?.apply { - when (currentViewIndex) { - 1, 2, 3, 4 -> switchView(0) - else -> default() - } - } - } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginView.kt index 2a5cf316a..a0949e6d9 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginView.kt @@ -1,19 +1,8 @@ package io.github.wulkanowy.ui.modules.login -import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.ui.base.BaseView interface LoginView : BaseView { - val currentViewIndex: Int - fun initView() - - fun switchView(index: Int) - - fun showActionBar(show: Boolean) - - fun notifyInitSymbolFragment(loginData: Triple) - - fun notifyInitStudentSelectFragment(studentsWithSemesters: List) } 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 bc29cd146..37dcb38b3 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 @@ -13,6 +13,7 @@ import io.github.wulkanowy.databinding.FragmentLoginAdvancedBinding import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.modules.login.LoginActivity +import io.github.wulkanowy.ui.modules.login.LoginData import io.github.wulkanowy.ui.modules.login.form.LoginSymbolAdapter import io.github.wulkanowy.utils.hideSoftInput import io.github.wulkanowy.utils.setOnEditorDoneSignIn @@ -80,6 +81,8 @@ class LoginAdvancedFragment : } override fun initView() { + (requireActivity() as LoginActivity).showActionBar(true) + hostKeys = resources.getStringArray(R.array.hosts_keys) hostValues = resources.getStringArray(R.array.hosts_values) hostSymbols = resources.getStringArray(R.array.hosts_symbols) @@ -320,14 +323,12 @@ class LoginAdvancedFragment : binding.loginFormContainer.visibility = if (show) VISIBLE else GONE } - 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] - ) - ) + override fun navigateToSymbol(loginData: LoginData) { + (activity as? LoginActivity)?.navigateToSymbolFragment(loginData) + } + + override fun navigateToStudentSelect(studentsWithSemesters: List) { + (activity as? LoginActivity)?.navigateToStudentSelect(studentsWithSemesters) } 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 17d8c5ecb..1b42c6c52 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 @@ -1,14 +1,16 @@ package io.github.wulkanowy.ui.modules.login.advanced -import io.github.wulkanowy.data.Status +import io.github.wulkanowy.data.Resource import io.github.wulkanowy.data.db.entities.StudentWithSemesters +import io.github.wulkanowy.data.logResourceStatus +import io.github.wulkanowy.data.onResourceNotLoading import io.github.wulkanowy.data.repositories.StudentRepository +import io.github.wulkanowy.data.resourceFlow import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.ui.base.BasePresenter +import io.github.wulkanowy.ui.modules.login.LoginData import io.github.wulkanowy.ui.modules.login.LoginErrorHandler import io.github.wulkanowy.utils.AnalyticsHelper -import io.github.wulkanowy.utils.afterLoading -import io.github.wulkanowy.utils.flowWithResource import io.github.wulkanowy.utils.ifNullOrBlank import kotlinx.coroutines.flow.onEach import timber.log.Timber @@ -77,7 +79,9 @@ class LoginAdvancedPresenter @Inject constructor( clearPassError() clearUsernameError() if (formHostValue.contains("fakelog")) { - setDefaultCredentials("jan@fakelog.cf", "jan123", "powiatwulkanowy", "FK100000", "999999") + setDefaultCredentials( + "jan@fakelog.cf", "jan123", "powiatwulkanowy", "FK100000", "999999" + ) } setSymbol(formHostSymbol) updateUsernameLabel() @@ -126,39 +130,47 @@ class LoginAdvancedPresenter @Inject constructor( fun onSignInClick() { if (!validateCredentials()) return - flowWithResource { getStudentsAppropriatesToLoginType() }.onEach { - when (it.status) { - Status.LOADING -> view?.run { - Timber.i("Login started") - hideSoftKeyboard() - showProgress(true) - showContent(false) - } - Status.SUCCESS -> { - Timber.i("Login result: Success") - analytics.logEvent("registration_form", + resourceFlow { getStudentsAppropriatesToLoginType() } + .logResourceStatus("login") + .onEach { + when (it) { + is Resource.Loading -> view?.run { + hideSoftKeyboard() + showProgress(true) + showContent(false) + } + is Resource.Success -> { + analytics.logEvent( + "registration_form", "success" to true, - "students" to it.data!!.size, + "students" to it.data.size, "error" to "No error" ) - view?.notifyParentAccountLogged(it.data) - } - Status.ERROR -> { - Timber.i("Login result: An exception occurred") - analytics.logEvent( - "registration_form", - "success" to false, "students" to -1, - "error" to it.error!!.message.ifNullOrBlank { "No message" } + val loginData = LoginData( + login = view?.formUsernameValue.orEmpty().trim(), + password = view?.formPassValue.orEmpty().trim(), + baseUrl = view?.formHostValue.orEmpty().trim() ) - loginErrorHandler.dispatch(it.error) + when (it.data.size) { + 0 -> view?.navigateToSymbol(loginData) + else -> view?.navigateToStudentSelect(it.data) + } + } + is Resource.Error -> { + analytics.logEvent( + "registration_form", + "success" to false, "students" to -1, + "error" to it.error.message.ifNullOrBlank { "No message" } + ) + loginErrorHandler.dispatch(it.error) + } } - } - }.afterLoading { - view?.apply { - showProgress(false) - showContent(true) - } - }.launch("login") + }.onResourceNotLoading { + view?.apply { + showProgress(false) + showContent(true) + } + }.launch("login") } private suspend fun getStudentsAppropriatesToLoginType(): List { @@ -172,8 +184,12 @@ class LoginAdvancedPresenter @Inject constructor( return when (Sdk.Mode.valueOf(view?.formLoginType.orEmpty())) { Sdk.Mode.API -> studentRepository.getStudentsApi(pin, symbol, token) - Sdk.Mode.SCRAPPER -> studentRepository.getStudentsScrapper(email, password, endpoint, symbol) - Sdk.Mode.HYBRID -> studentRepository.getStudentsHybrid(email, password, endpoint, symbol) + Sdk.Mode.SCRAPPER -> studentRepository.getStudentsScrapper( + email, password, endpoint, symbol + ) + Sdk.Mode.HYBRID -> studentRepository.getStudentsHybrid( + email, password, endpoint, symbol + ) } } 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 1d2b2856d..f9b84f1ab 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 @@ -2,6 +2,7 @@ package io.github.wulkanowy.ui.modules.login.advanced import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.ui.base.BaseView +import io.github.wulkanowy.ui.modules.login.LoginData interface LoginAdvancedView : BaseView { @@ -69,7 +70,9 @@ interface LoginAdvancedView : BaseView { fun showContent(show: Boolean) - fun notifyParentAccountLogged(studentsWithSemesters: List) + fun navigateToSymbol(loginData: LoginData) + + fun navigateToStudentSelect(studentsWithSemesters: List) fun setErrorPinRequired() 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 c741da429..d31f5cf0f 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 @@ -13,6 +13,7 @@ import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.databinding.FragmentLoginFormBinding import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.modules.login.LoginActivity +import io.github.wulkanowy.ui.modules.login.LoginData import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.hideSoftInput import io.github.wulkanowy.utils.openEmailClient @@ -68,6 +69,8 @@ class LoginFormFragment : BaseFragment(R.layout.fragme } override fun initView() { + (requireActivity() as LoginActivity).showActionBar(false) + hostKeys = resources.getStringArray(R.array.hosts_keys) hostValues = resources.getStringArray(R.array.hosts_values) hostSymbols = resources.getStringArray(R.array.hosts_symbols) @@ -203,11 +206,9 @@ class LoginFormFragment : BaseFragment(R.layout.fragme binding.loginFormVersion.text = "v${appInfo.versionName}" } - override fun notifyParentAccountLogged( - studentsWithSemesters: List, - loginData: Triple - ) { - (activity as? LoginActivity)?.onFormFragmentAccountLogged(studentsWithSemesters, loginData) + override fun showContact(show: Boolean) { + binding.loginFormContact.visibility = if (show) VISIBLE else GONE + binding.loginFormRecoverLink.visibility = if (show) GONE else VISIBLE } override fun openPrivacyPolicyPage() { @@ -217,9 +218,12 @@ class LoginFormFragment : BaseFragment(R.layout.fragme ) } - override fun showContact(show: Boolean) { - binding.loginFormContact.visibility = if (show) VISIBLE else GONE - binding.loginFormRecoverLink.visibility = if (show) GONE else VISIBLE + override fun navigateToSymbol(loginData: LoginData) { + (activity as? LoginActivity)?.navigateToSymbolFragment(loginData) + } + + override fun navigateToStudentSelect(studentsWithSemesters: List) { + (activity as? LoginActivity)?.navigateToStudentSelect(studentsWithSemesters) } override fun openAdvancedLogin() { 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 1002549cf..0acb0ea6d 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 @@ -1,15 +1,13 @@ package io.github.wulkanowy.ui.modules.login.form import androidx.core.net.toUri -import io.github.wulkanowy.data.Status +import io.github.wulkanowy.data.* import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.ui.base.BasePresenter +import io.github.wulkanowy.ui.modules.login.LoginData import io.github.wulkanowy.ui.modules.login.LoginErrorHandler import io.github.wulkanowy.utils.AnalyticsHelper -import io.github.wulkanowy.utils.afterLoading -import io.github.wulkanowy.utils.flowWithResource import io.github.wulkanowy.utils.ifNullOrBlank -import kotlinx.coroutines.flow.onEach import timber.log.Timber import java.net.URL import javax.inject.Inject @@ -74,7 +72,7 @@ class LoginFormPresenter @Inject constructor( val username = view?.formUsernameValue.orEmpty().trim() if ("@" in username && "@vulcan" !in username) { - val hosts = view?.getHostsValues().orEmpty().map { it.toUri().host to it }.toMap() + val hosts = view?.getHostsValues().orEmpty().associateBy { it.toUri().host } val usernameHost = username.substringAfter("@") hosts[usernameHost]?.let { @@ -94,51 +92,54 @@ class LoginFormPresenter @Inject constructor( if (!validateCredentials(email, password, host)) return - flowWithResource { + resourceFlow { studentRepository.getStudentsScrapper( email = email, password = password, scrapperBaseUrl = host, symbol = symbol ) - }.onEach { - when (it.status) { - Status.LOADING -> view?.run { - Timber.i("Login started") + } + .logResourceStatus("login") + .onResourceLoading { + view?.run { hideSoftKeyboard() showProgress(true) showContent(false) } - Status.SUCCESS -> { - Timber.i("Login result: Success") - analytics.logEvent( - "registration_form", - "success" to true, - "students" to it.data!!.size, - "scrapperBaseUrl" to host, - "error" to "No error" - ) - view?.notifyParentAccountLogged(it.data, Triple(email, password, host)) + } + .onResourceSuccess { + when (it.size) { + 0 -> view?.navigateToSymbol(LoginData(email, password, host)) + else -> view?.navigateToStudentSelect(it) } - Status.ERROR -> { - Timber.i("Login result: An exception occurred") - analytics.logEvent( - "registration_form", - "success" to false, - "students" to -1, - "scrapperBaseUrl" to host, - "error" to it.error!!.message.ifNullOrBlank { "No message" }) - loginErrorHandler.dispatch(it.error) - lastError = it.error - view?.showContact(true) + analytics.logEvent( + "registration_form", + "success" to true, + "students" to it.size, + "scrapperBaseUrl" to host, + "error" to "No error" + ) + } + .onResourceNotLoading { + view?.apply { + showProgress(false) + showContent(true) } } - }.afterLoading { - view?.apply { - showProgress(false) - showContent(true) + .onResourceError { + loginErrorHandler.dispatch(it) + lastError = it + view?.showContact(true) + analytics.logEvent( + "registration_form", + "success" to false, + "students" to -1, + "scrapperBaseUrl" to host, + "error" to it.message.ifNullOrBlank { "No message" } + ) } - }.launch("login") + .launch("login") } fun onFaqClick() { @@ -171,7 +172,7 @@ class LoginFormPresenter @Inject constructor( if ("@" in login && "||" !in login && "login" !in host && "email" !in host) { val emailHost = login.substringAfter("@") val emailDomain = URL(host).host - if (emailHost != emailDomain) { + if (!emailHost.equals(emailDomain, true)) { view?.setErrorEmailInvalid(domain = emailDomain) isCorrect = false } 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 300573559..8003975db 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 @@ -2,6 +2,7 @@ package io.github.wulkanowy.ui.modules.login.form import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.ui.base.BaseView +import io.github.wulkanowy.ui.modules.login.LoginData interface LoginFormView : BaseView { @@ -57,7 +58,9 @@ interface LoginFormView : BaseView { fun showVersion() - fun notifyParentAccountLogged(studentsWithSemesters: List, loginData: Triple) + fun navigateToSymbol(loginData: LoginData) + + fun navigateToStudentSelect(studentsWithSemesters: List) fun openPrivacyPolicyPage() 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 a91dfb618..786bbfce8 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 @@ -6,13 +6,13 @@ import android.os.Bundle import android.view.View import android.view.View.GONE import android.view.View.VISIBLE -import android.webkit.JavascriptInterface -import android.webkit.WebView -import android.webkit.WebViewClient +import android.webkit.* import androidx.core.view.isVisible import androidx.core.widget.doOnTextChanged +import com.yariksoffice.lingver.Lingver import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R +import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.databinding.FragmentLoginRecoverBinding import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.modules.login.LoginActivity @@ -32,6 +32,12 @@ class LoginRecoverFragment : @Inject lateinit var presenter: LoginRecoverPresenter + @Inject + lateinit var lingver: Lingver + + @Inject + lateinit var preferencesRepository: PreferencesRepository + companion object { fun newInstance() = LoginRecoverFragment() } @@ -64,11 +70,23 @@ class LoginRecoverFragment : override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + restoreCorrectLocale() _binding = FragmentLoginRecoverBinding.bind(view) presenter.onAttachView(this) } + // https://issuetracker.google.com/issues/37113860 + private fun restoreCorrectLocale() { + if (preferencesRepository.appLanguage == "system") { + lingver.setFollowSystemLocale(requireContext()) + } else { + lingver.setLocale(requireContext(), lingver.getLocale()) + } + } + override fun initView() { + (requireActivity() as LoginActivity).showActionBar(true) + hostKeys = resources.getStringArray(R.array.hosts_keys) hostValues = resources.getStringArray(R.array.hosts_values) hostSymbols = resources.getStringArray(R.array.hosts_symbols) @@ -80,7 +98,7 @@ class LoginRecoverFragment : loginRecoverButton.setOnClickListener { presenter.onRecoverClick() } loginRecoverErrorRetry.setOnClickListener { presenter.onRecoverClick() } loginRecoverErrorDetails.setOnClickListener { presenter.onDetailsClick() } - loginRecoverLogin.setOnClickListener { (activity as LoginActivity).switchView(0) } + loginRecoverLogin.setOnClickListener { (activity as LoginActivity).onBackPressed() } } with(bindingLocal.loginRecoverHost) { @@ -186,10 +204,9 @@ class LoginRecoverFragment : } override fun onReceivedError( - view: WebView, - errorCode: Int, - description: String, - failingUrl: String + view: WebView?, + request: WebResourceRequest?, + error: WebResourceError? ) { recoverWebViewSuccess = false } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/recover/LoginRecoverPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/recover/LoginRecoverPresenter.kt index 271e8a8a0..3d0493012 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/recover/LoginRecoverPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/recover/LoginRecoverPresenter.kt @@ -1,12 +1,12 @@ package io.github.wulkanowy.ui.modules.login.recover -import io.github.wulkanowy.data.Status +import io.github.wulkanowy.data.Resource +import io.github.wulkanowy.data.onResourceNotLoading import io.github.wulkanowy.data.repositories.RecoverRepository import io.github.wulkanowy.data.repositories.StudentRepository +import io.github.wulkanowy.data.resourceFlow import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.utils.AnalyticsHelper -import io.github.wulkanowy.utils.afterLoading -import io.github.wulkanowy.utils.flowWithResource import io.github.wulkanowy.utils.ifNullOrBlank import kotlinx.coroutines.flow.onEach import timber.log.Timber @@ -57,24 +57,28 @@ class LoginRecoverPresenter @Inject constructor( if (!validateInput(username, host)) return - flowWithResource { recoverRepository.getReCaptchaSiteKey(host, symbol.ifBlank { "Default" }) }.onEach { - when (it.status) { - Status.LOADING -> view?.run { + resourceFlow { + recoverRepository.getReCaptchaSiteKey( + host, + symbol.ifBlank { "Default" }) + }.onEach { + when (it) { + is Resource.Loading -> view?.run { hideSoftKeyboard() showRecoverForm(false) showProgress(true) showErrorView(false) showCaptcha(false) } - Status.SUCCESS -> view?.run { - loadReCaptcha(url = it.data!!.first, siteKey = it.data.second) + is Resource.Success -> view?.run { + loadReCaptcha(url = it.data.first, siteKey = it.data.second) showProgress(false) showErrorView(false) showCaptcha(true) } - Status.ERROR -> { + is Resource.Error -> { Timber.i("Obtain captcha site key result: An exception occurred") - errorHandler.dispatch(it.error!!) + errorHandler.dispatch(it.error) } } }.launch("captcha") @@ -101,26 +105,43 @@ class LoginRecoverPresenter @Inject constructor( val host = view?.recoverHostValue.orEmpty() val symbol = view?.formHostSymbol.ifNullOrBlank { "Default" } - flowWithResource { recoverRepository.sendRecoverRequest(host, symbol, username, reCaptchaResponse) }.onEach { - when (it.status) { - Status.LOADING -> view?.run { + resourceFlow { + recoverRepository.sendRecoverRequest( + host, + symbol, + username, + reCaptchaResponse + ) + }.onEach { + when (it) { + is Resource.Loading -> view?.run { showProgress(true) showRecoverForm(false) showCaptcha(false) } - Status.SUCCESS -> view?.run { + is Resource.Success -> view?.run { showSuccessView(true) - setSuccessTitle(it.data!!.substringBefore(". ")) + setSuccessTitle(it.data.substringBefore(". ")) setSuccessMessage(it.data.substringAfter(". ")) - analytics.logEvent("account_recover", "register" to host, "symbol" to symbol, "success" to true) + analytics.logEvent( + "account_recover", + "register" to host, + "symbol" to symbol, + "success" to true + ) } - Status.ERROR -> { + is Resource.Error -> { Timber.i("Send recover request result: An exception occurred") - errorHandler.dispatch(it.error!!) - analytics.logEvent("account_recover", "register" to host, "symbol" to symbol, "success" to false) + errorHandler.dispatch(it.error) + analytics.logEvent( + "account_recover", + "register" to host, + "symbol" to symbol, + "success" to false + ) } } - }.afterLoading { + }.onResourceNotLoading { view?.showProgress(false) }.launch("verified") } 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 ac4c03130..28686d626 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 @@ -8,8 +8,9 @@ import io.github.wulkanowy.sdk.scrapper.exception.NoAccountFoundException import io.github.wulkanowy.ui.base.ErrorHandler import javax.inject.Inject -class RecoverErrorHandler @Inject constructor(@ApplicationContext context: Context) : - ErrorHandler(context) { +class RecoverErrorHandler @Inject constructor( + @ApplicationContext context: Context, +) : ErrorHandler(context) { var onInvalidUsername: (String) -> Unit = {} 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 87cb505c4..6c910fe03 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 @@ -4,17 +4,18 @@ import android.os.Bundle import android.view.View import android.view.View.GONE import android.view.View.VISIBLE +import androidx.core.os.bundleOf import androidx.recyclerview.widget.LinearLayoutManager import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.databinding.FragmentLoginStudentSelectBinding import io.github.wulkanowy.ui.base.BaseFragment +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.openEmailClient import io.github.wulkanowy.utils.openInternetBrowser -import java.io.Serializable import javax.inject.Inject @AndroidEntryPoint @@ -32,18 +33,27 @@ class LoginStudentSelectFragment : lateinit var appInfo: AppInfo companion object { - const val SAVED_STUDENTS = "STUDENTS" + const val ARG_STUDENTS = "STUDENTS" - fun newInstance() = LoginStudentSelectFragment() + fun newInstance(studentsWithSemesters: List) = + LoginStudentSelectFragment().apply { + arguments = bundleOf(ARG_STUDENTS to studentsWithSemesters) + } } + @Suppress("UNCHECKED_CAST") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding = FragmentLoginStudentSelectBinding.bind(view) - presenter.onAttachView(this, savedInstanceState?.getSerializable(SAVED_STUDENTS)) + presenter.onAttachView( + view = this, + students = requireArguments().getSerializable(ARG_STUDENTS) as List, + ) } override fun initView() { + (requireActivity() as LoginActivity).showActionBar(true) + loginAdapter.onClickListener = presenter::onItemSelected with(binding) { @@ -82,15 +92,6 @@ class LoginStudentSelectFragment : binding.loginStudentSelectSignIn.isEnabled = enable } - fun onParentInitStudentSelectFragment(studentsWithSemesters: List) { - presenter.onParentInitStudentSelectView(studentsWithSemesters) - } - - override fun onSaveInstanceState(outState: Bundle) { - super.onSaveInstanceState(outState) - outState.putSerializable(SAVED_STUDENTS, presenter.students as Serializable) - } - override fun showContact(show: Boolean) { binding.loginStudentSelectContact.visibility = if (show) VISIBLE else GONE } 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 f0f5586cc..3455b3cf1 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 @@ -1,32 +1,32 @@ package io.github.wulkanowy.ui.modules.login.studentselect -import io.github.wulkanowy.data.Status +import io.github.wulkanowy.data.Resource import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.StudentWithSemesters +import io.github.wulkanowy.data.logResourceStatus import io.github.wulkanowy.data.repositories.StudentRepository +import io.github.wulkanowy.data.resourceFlow +import io.github.wulkanowy.services.sync.SyncManager import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.modules.login.LoginErrorHandler import io.github.wulkanowy.utils.AnalyticsHelper -import io.github.wulkanowy.utils.flowWithResource import io.github.wulkanowy.utils.ifNullOrBlank import kotlinx.coroutines.flow.onEach import timber.log.Timber -import java.io.Serializable import javax.inject.Inject class LoginStudentSelectPresenter @Inject constructor( studentRepository: StudentRepository, private val loginErrorHandler: LoginErrorHandler, + private val syncManager: SyncManager, private val analytics: AnalyticsHelper ) : BasePresenter(loginErrorHandler, studentRepository) { private var lastError: Throwable? = null - var students = emptyList() - private val selectedStudents = mutableListOf() - fun onAttachView(view: LoginStudentSelectView, students: Serializable?) { + fun onAttachView(view: LoginStudentSelectView, students: List) { super.onAttachView(view) with(view) { initView() @@ -38,20 +38,14 @@ class LoginStudentSelectPresenter @Inject constructor( } } - if (students is List<*> && students.isNotEmpty()) { - loadData(students.filterIsInstance()) - } + if (students.size == 1) registerStudents(students) + loadData(students) } fun onSignIn() { registerStudents(selectedStudents) } - fun onParentInitStudentSelectView(studentsWithSemesters: List) { - loadData(studentsWithSemesters) - if (studentsWithSemesters.size == 1) registerStudents(studentsWithSemesters) - } - fun onItemSelected(studentWithSemester: StudentWithSemesters, alreadySaved: Boolean) { if (alreadySaved) return @@ -72,18 +66,17 @@ class LoginStudentSelectPresenter @Inject constructor( private fun loadData(studentsWithSemesters: List) { resetSelectedState() - this.students = studentsWithSemesters - flowWithResource { studentRepository.getSavedStudents(false) }.onEach { - 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 -> + resourceFlow { studentRepository.getSavedStudents(false) }.onEach { + when (it) { + is Resource.Loading -> Timber.d("Login student select students load started") + is Resource.Success -> view?.updateData(studentsWithSemesters.map { studentWithSemesters -> + studentWithSemesters to it.data.any { item -> compareStudents(studentWithSemesters.student, item.student) } }) - Status.ERROR -> { - errorHandler.dispatch(it.error!!) + is Resource.Error -> { + errorHandler.dispatch(it.error) lastError = it.error view?.updateData(studentsWithSemesters.map { student -> student to false }) } @@ -97,28 +90,27 @@ class LoginStudentSelectPresenter @Inject constructor( } private fun registerStudents(studentsWithSemesters: List) { - flowWithResource { studentRepository.saveStudents(studentsWithSemesters) } + resourceFlow { studentRepository.saveStudents(studentsWithSemesters) } + .logResourceStatus("registration") .onEach { - when (it.status) { - Status.LOADING -> view?.run { - Timber.i("Registration started") + when (it) { + is Resource.Loading -> view?.run { showProgress(true) showContent(false) } - Status.SUCCESS -> { - Timber.i("Registration result: Success") + is Resource.Success -> { + syncManager.startOneTimeSyncWorker(quiet = true) view?.openMainView() logRegisterEvent(studentsWithSemesters) } - Status.ERROR -> { - Timber.i("Registration result: An exception occurred ") + is Resource.Error -> { view?.apply { showProgress(false) showContent(true) showContact(true) } lastError = it.error - loginErrorHandler.dispatch(it.error!!) + loginErrorHandler.dispatch(it.error) logRegisterEvent(studentsWithSemesters, it.error) } } @@ -143,7 +135,8 @@ class LoginStudentSelectPresenter @Inject constructor( "success" to (error != null), "scrapperBaseUrl" to student.student.scrapperBaseUrl, "symbol" to student.student.symbol, - "error" to (error?.message?.ifBlank { "No message" } ?: "No error")) + "error" to (error?.message?.ifBlank { "No message" } ?: "No error") + ) } } } 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 a8086935b..58bdf6cef 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.os.bundleOf import androidx.core.text.parseAsHtml import androidx.core.widget.doOnTextChanged import dagger.hilt.android.AndroidEntryPoint @@ -15,6 +16,7 @@ import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.databinding.FragmentLoginSymbolBinding import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.modules.login.LoginActivity +import io.github.wulkanowy.ui.modules.login.LoginData import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.hideSoftInput import io.github.wulkanowy.utils.openEmailClient @@ -35,7 +37,9 @@ class LoginSymbolFragment : companion object { private const val SAVED_LOGIN_DATA = "LOGIN_DATA" - fun newInstance() = LoginSymbolFragment() + fun newInstance(loginData: LoginData) = LoginSymbolFragment().apply { + arguments = bundleOf(SAVED_LOGIN_DATA to loginData) + } } override val symbolNameError: CharSequence? @@ -44,10 +48,15 @@ class LoginSymbolFragment : override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding = FragmentLoginSymbolBinding.bind(view) - presenter.onAttachView(this, savedInstanceState?.getSerializable(SAVED_LOGIN_DATA)) + presenter.onAttachView( + view = this, + loginData = requireArguments().getSerializable(SAVED_LOGIN_DATA) as LoginData, + ) } override fun initView() { + (requireActivity() as LoginActivity).showActionBar(true) + with(binding) { loginSymbolSignIn.setOnClickListener { presenter.attemptLogin(loginSymbolName.text.toString()) } loginSymbolFaq.setOnClickListener { presenter.onFaqClick() } @@ -70,12 +79,9 @@ class LoginSymbolFragment : } } - fun onParentInitSymbolFragment(loginData: Triple) { - presenter.onParentInitSymbolView(loginData) - } - override fun setLoginToHeading(login: String) { - binding.loginSymbolHeader.text = getString(R.string.login_header_symbol, login).parseAsHtml() + binding.loginSymbolHeader.text = + getString(R.string.login_header_symbol, login).parseAsHtml() } override fun setErrorSymbolIncorrect() { @@ -119,8 +125,8 @@ class LoginSymbolFragment : binding.loginSymbolContainer.visibility = if (show) VISIBLE else GONE } - override fun notifyParentAccountLogged(studentsWithSemesters: List) { - (activity as? LoginActivity)?.onSymbolFragmentAccountLogged(studentsWithSemesters) + override fun navigateToStudentSelect(studentsWithSemesters: List) { + (activity as? LoginActivity)?.navigateToStudentSelect(studentsWithSemesters) } override fun onSaveInstanceState(outState: Bundle) { 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 1ba7e5b32..691cd4481 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 @@ -1,16 +1,16 @@ package io.github.wulkanowy.ui.modules.login.symbol -import io.github.wulkanowy.data.Status +import io.github.wulkanowy.data.Resource +import io.github.wulkanowy.data.onResourceNotLoading import io.github.wulkanowy.data.repositories.StudentRepository +import io.github.wulkanowy.data.resourceFlow import io.github.wulkanowy.ui.base.BasePresenter +import io.github.wulkanowy.ui.modules.login.LoginData import io.github.wulkanowy.ui.modules.login.LoginErrorHandler import io.github.wulkanowy.utils.AnalyticsHelper -import io.github.wulkanowy.utils.afterLoading -import io.github.wulkanowy.utils.flowWithResource import io.github.wulkanowy.utils.ifNullOrBlank import kotlinx.coroutines.flow.onEach import timber.log.Timber -import java.io.Serializable import javax.inject.Inject class LoginSymbolPresenter @Inject constructor( @@ -21,25 +21,15 @@ class LoginSymbolPresenter @Inject constructor( private var lastError: Throwable? = null - var loginData: Triple? = null + lateinit var loginData: LoginData - @Suppress("UNCHECKED_CAST") - fun onAttachView(view: LoginSymbolView, savedLoginData: Serializable?) { + fun onAttachView(view: LoginSymbolView, loginData: LoginData) { super.onAttachView(view) - view.run { + this.loginData = loginData + with(view) { initView() showContact(false) - } - 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) + setLoginToHeading(loginData.login) clearAndFocusSymbol() showSoftKeyboard() } @@ -50,67 +40,65 @@ class LoginSymbolPresenter @Inject constructor( } fun attemptLogin(symbol: String) { - if (loginData == null) { - Timber.w("LoginSymbolPresenter - Login data is null") - return - } - if (symbol.isBlank()) { view?.setErrorSymbolRequire() return } - flowWithResource { + resourceFlow { studentRepository.getStudentsScrapper( - email = loginData!!.first, - password = loginData!!.second, - scrapperBaseUrl = loginData!!.third, + email = loginData.login, + password = loginData.password, + scrapperBaseUrl = loginData.baseUrl, symbol = symbol, ) }.onEach { - when (it.status) { - Status.LOADING -> view?.run { + when (it) { + is Resource.Loading -> view?.run { Timber.i("Login with symbol started") hideSoftKeyboard() showProgress(true) showContent(false) } - Status.SUCCESS -> { - view?.run { - if (it.data!!.isEmpty()) { + is Resource.Success -> { + when (it.data.size) { + 0 -> { Timber.i("Login with symbol result: Empty student list") - setErrorSymbolIncorrect() - view?.showContact(true) - } else { + view?.run { + setErrorSymbolIncorrect() + showContact(true) + } + } + else -> { Timber.i("Login with symbol result: Success") - notifyParentAccountLogged(it.data) + view?.navigateToStudentSelect(requireNotNull(it.data)) } } analytics.logEvent( "registration_symbol", "success" to true, - "students" to it.data!!.size, - "scrapperBaseUrl" to loginData?.third, + "students" to it.data.size, + "scrapperBaseUrl" to loginData.baseUrl, "symbol" to symbol, "error" to "No error" ) } - Status.ERROR -> { + is Resource.Error -> { Timber.i("Login with symbol result: An exception occurred") analytics.logEvent( "registration_symbol", "success" to false, "students" to -1, - "scrapperBaseUrl" to loginData?.third, + "scrapperBaseUrl" to loginData.baseUrl, "symbol" to symbol, - "error" to it.error!!.message.ifNullOrBlank { "No message" } + "error" to it.error.message.ifNullOrBlank { "No message" } ) loginErrorHandler.dispatch(it.error) lastError = it.error view?.showContact(true) } } - }.afterLoading { + }.onResourceNotLoading { view?.apply { showProgress(false) showContent(true) @@ -123,6 +111,6 @@ class LoginSymbolPresenter @Inject constructor( } fun onEmailClick() { - view?.openEmail(loginData?.third.orEmpty(), lastError?.message.ifNullOrBlank { "empty" }) + view?.openEmail(loginData.baseUrl, lastError?.message.ifNullOrBlank { "empty" }) } } 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 75523a7c7..527895b77 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 @@ -27,7 +27,7 @@ interface LoginSymbolView : BaseView { fun showContent(show: Boolean) - fun notifyParentAccountLogged(studentsWithSemesters: List) + fun navigateToStudentSelect(studentsWithSemesters: List) fun showContact(show: Boolean) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/LuckyNumberPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/LuckyNumberPresenter.kt index fd0598d8f..6f5c8e740 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/LuckyNumberPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/LuckyNumberPresenter.kt @@ -1,14 +1,11 @@ package io.github.wulkanowy.ui.modules.luckynumber -import io.github.wulkanowy.data.Status +import io.github.wulkanowy.data.* import io.github.wulkanowy.data.repositories.LuckyNumberRepository 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.afterLoading -import io.github.wulkanowy.utils.flowWithResourceIn -import kotlinx.coroutines.flow.onEach import timber.log.Timber import javax.inject.Inject @@ -34,47 +31,45 @@ class LuckyNumberPresenter @Inject constructor( } private fun loadData(forceRefresh: Boolean = false) { - flowWithResourceIn { + flatResourceFlow { val student = studentRepository.getCurrentStudent() luckyNumberRepository.getLuckyNumber(student, forceRefresh) - }.onEach { - when (it.status) { - Status.LOADING -> Timber.i("Loading lucky number started") - Status.SUCCESS -> { - if (it.data != null) { - Timber.i("Loading lucky number result: Success") - view?.apply { - updateData(it.data) - showContent(true) - showEmpty(false) - showErrorView(false) - } - analytics.logEvent( - "load_item", - "type" to "lucky_number", - "number" to it.data.luckyNumber - ) - } else { - Timber.i("Loading lucky number result: No lucky number found") - view?.run { - showContent(false) - showEmpty(true) - showErrorView(false) - } + } + .logResourceStatus("load lucky number") + .onResourceData { + if (it != null) { + view?.apply { + updateData(it) + showContent(true) + showEmpty(false) + showErrorView(false) + } + } else { + view?.run { + showContent(false) + showEmpty(true) + showErrorView(false) } } - Status.ERROR -> { - Timber.i("Loading lucky number result: An exception occurred") - errorHandler.dispatch(it.error!!) + } + .onResourceSuccess { + if (it != null) { + analytics.logEvent( + "load_item", + "type" to "lucky_number", + "number" to it.luckyNumber + ) } } - }.afterLoading { - view?.run { - hideRefresh() - showProgress(false) - enableSwipe(true) + .onResourceNotLoading { + view?.run { + hideRefresh() + showProgress(false) + enableSwipe(true) + } } - }.launch() + .onResourceError(errorHandler::dispatch) + .launch() } private fun showErrorViewOnError(message: String, error: Throwable) { 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 49d094b78..53f06cacd 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 @@ -5,8 +5,6 @@ import android.view.View import android.view.View.GONE import android.view.View.VISIBLE import androidx.recyclerview.widget.LinearLayoutManager -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.data.db.entities.LuckyNumber @@ -14,11 +12,9 @@ import io.github.wulkanowy.databinding.FragmentLuckyNumberHistoryBinding import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.widgets.DividerItemDecoration -import io.github.wulkanowy.utils.SchoolDaysValidator import io.github.wulkanowy.utils.dpToPx -import io.github.wulkanowy.utils.schoolYearStart -import io.github.wulkanowy.utils.toLocalDateTime -import io.github.wulkanowy.utils.toTimestamp +import io.github.wulkanowy.utils.firstSchoolDayInSchoolYear +import io.github.wulkanowy.utils.openMaterialDatePicker import java.time.LocalDate import javax.inject.Inject @@ -111,29 +107,15 @@ class LuckyNumberHistoryFragment : binding.luckyNumberHistoryNextButton.visibility = if (show) VISIBLE else View.INVISIBLE } - override fun showDatePickerDialog(currentDate: LocalDate) { - val baseDate = currentDate.schoolYearStart - val rangeStart = baseDate.toTimestamp() - val rangeEnd = LocalDate.now().plusWeeks(1).toTimestamp() - - val constraintsBuilder = CalendarConstraints.Builder().apply { - setValidator(SchoolDaysValidator(rangeStart, rangeEnd)) - setStart(rangeStart) - setEnd(rangeEnd) - } - val datePicker = MaterialDatePicker.Builder.datePicker() - .setCalendarConstraints(constraintsBuilder.build()) - .setSelection(currentDate.toTimestamp()) - .build() - - datePicker.addOnPositiveButtonClickListener { - val date = it.toLocalDateTime() - presenter.onDateSet(date.year, date.monthValue, date.dayOfMonth) - } - - if (!parentFragmentManager.isStateSaved) { - datePicker.show(parentFragmentManager, null) - } + override fun showDatePickerDialog(selectedDate: LocalDate) { + openMaterialDatePicker( + selected = selectedDate, + rangeStart = selectedDate.firstSchoolDayInSchoolYear, + rangeEnd = LocalDate.now().plusWeeks(1), + onDateSelected = { + presenter.onDateSet(it.year, it.monthValue, it.dayOfMonth) + } + ) } override fun showContent(show: Boolean) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/history/LuckyNumberHistoryPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/history/LuckyNumberHistoryPresenter.kt index c45cb69a7..fc753950b 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/history/LuckyNumberHistoryPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/history/LuckyNumberHistoryPresenter.kt @@ -1,24 +1,12 @@ package io.github.wulkanowy.ui.modules.luckynumber.history -import io.github.wulkanowy.data.Status import io.github.wulkanowy.data.repositories.LuckyNumberRepository 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.AnalyticsHelper -import io.github.wulkanowy.utils.afterLoading -import io.github.wulkanowy.utils.flowWithResource -import io.github.wulkanowy.utils.getLastSchoolDayIfHoliday -import io.github.wulkanowy.utils.isHolidays -import io.github.wulkanowy.utils.monday -import io.github.wulkanowy.utils.previousOrSameSchoolDay -import io.github.wulkanowy.utils.sunday -import io.github.wulkanowy.utils.toFormattedString -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.onEach +import io.github.wulkanowy.utils.* +import kotlinx.coroutines.flow.* import timber.log.Timber import java.time.LocalDate import javax.inject.Inject @@ -52,55 +40,51 @@ class LuckyNumberHistoryPresenter @Inject constructor( flow { val student = studentRepository.getCurrentStudent() emit(semesterRepository.getCurrentSemester(student)) - }.catch { - Timber.i("Loading semester result: An exception occurred") - }.onEach { - currentDate = currentDate.getLastSchoolDayIfHoliday(it.schoolYear) - reloadNavigation() - }.launch("holidays") + } + .catch { Timber.i("Loading semester result: An exception occurred") } + .onEach { + currentDate = currentDate.getLastSchoolDayIfHoliday(it.schoolYear) + reloadNavigation() + } + .launch("holidays") } private fun loadData() { - flowWithResource { + flow { val student = studentRepository.getCurrentStudent() - luckyNumberRepository.getLuckyNumberHistory(student, currentDate.monday, currentDate.sunday) - }.onEach { - when (it.status) { - Status.LOADING -> Timber.i("Loading lucky number history started") - Status.SUCCESS -> { - if (!it.data?.first().isNullOrEmpty()) { - Timber.i("Loading lucky number result: Success") - view?.apply { - updateData(it.data!!.first()) - showContent(true) - showEmpty(false) - showErrorView(false) - showProgress(false) - } - analytics.logEvent( - "load_items", - "type" to "lucky_number_history", - "numbers" to it.data - ) - } else { - Timber.i("Loading lucky number history result: No lucky numbers found") - view?.run { - showContent(false) - showEmpty(true) - showErrorView(false) - } + emitAll( + luckyNumberRepository.getLuckyNumberHistory( + student = student, + start = currentDate.monday, + end = currentDate.sunday + ) + ) + } + .onEach { + if (!it.isNullOrEmpty()) { + view?.apply { + updateData(it) + showContent(true) + showEmpty(false) + showErrorView(false) + showProgress(false) + } + } else { + view?.run { + showContent(false) + showEmpty(true) + showErrorView(false) + showProgress(false) } } - Status.ERROR -> { - Timber.i("Loading lucky number history result: An exception occurred") - errorHandler.dispatch(it.error!!) - } + + analytics.logEvent( + "load_items", + "type" to "lucky_number_history", + ) } - }.afterLoading { - view?.run { - showProgress(false) - } - }.launch() + .catch { errorHandler.dispatch(it) } + .launchIn(presenterScope) } private fun showErrorViewOnError(message: String, error: Throwable) { @@ -143,8 +127,10 @@ class LuckyNumberHistoryPresenter @Inject constructor( view?.apply { showPreButton(!currentDate.minusDays(7).isHolidays) showNextButton(!currentDate.plusDays(7).isHolidays) - updateNavigationWeek("${currentDate.monday.toFormattedString("dd.MM")} - " + - currentDate.sunday.toFormattedString("dd.MM")) + updateNavigationWeek( + "${currentDate.monday.toFormattedString("dd.MM")} - " + + currentDate.sunday.toFormattedString("dd.MM") + ) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/history/LuckyNumberHistoryView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/history/LuckyNumberHistoryView.kt index 331e4ff86..7b9b0294f 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/history/LuckyNumberHistoryView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/history/LuckyNumberHistoryView.kt @@ -28,7 +28,7 @@ interface LuckyNumberHistoryView : BaseView { fun showNextButton(show: Boolean) - fun showDatePickerDialog(currentDate: LocalDate) + fun showDatePickerDialog(selectedDate: LocalDate) fun showContent(show: Boolean) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetConfigurePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetConfigurePresenter.kt index 5b6af69ac..cac648da8 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetConfigurePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetConfigurePresenter.kt @@ -1,14 +1,14 @@ package io.github.wulkanowy.ui.modules.luckynumberwidget -import io.github.wulkanowy.data.Status +import io.github.wulkanowy.data.Resource import io.github.wulkanowy.data.db.SharedPrefProvider import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.repositories.StudentRepository +import io.github.wulkanowy.data.resourceFlow import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.ui.modules.luckynumberwidget.LuckyNumberWidgetProvider.Companion.getStudentWidgetKey import io.github.wulkanowy.ui.modules.luckynumberwidget.LuckyNumberWidgetProvider.Companion.getThemeWidgetKey -import io.github.wulkanowy.utils.flowWithResource import kotlinx.coroutines.flow.onEach import timber.log.Timber import javax.inject.Inject @@ -47,16 +47,15 @@ class LuckyNumberWidgetConfigurePresenter @Inject constructor( } private fun loadData() { - flowWithResource { studentRepository.getSavedStudents(false) }.onEach { - when (it.status) { - Status.LOADING -> Timber.d("Lucky number widget configure students data load") - Status.SUCCESS -> { + resourceFlow { studentRepository.getSavedStudents(false) }.onEach { + when (it) { + is Resource.Loading -> Timber.d("Lucky number widget configure students data load") + is Resource.Success -> { val selectedStudentId = appWidgetId?.let { id -> sharedPref.getLong(getStudentWidgetKey(id), 0) } ?: -1 - when { - it.data!!.isEmpty() -> view?.openLoginView() + it.data.isEmpty() -> view?.openLoginView() it.data.size == 1 -> { selectedStudent = it.data.single().student view?.showThemeDialog() @@ -64,7 +63,7 @@ class LuckyNumberWidgetConfigurePresenter @Inject constructor( else -> view?.updateData(it.data, selectedStudentId) } } - Status.ERROR -> errorHandler.dispatch(it.error!!) + is Resource.Error -> errorHandler.dispatch(it.error) } }.launch() } 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 2b2d18fa0..e03e3e90e 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 @@ -13,14 +13,17 @@ import android.view.View.VISIBLE import android.widget.RemoteViews import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R +import io.github.wulkanowy.data.Resource +import io.github.wulkanowy.data.dataOrNull import io.github.wulkanowy.data.db.SharedPrefProvider +import io.github.wulkanowy.data.db.entities.LuckyNumber 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.data.toFirstResult 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 import javax.inject.Inject @@ -66,12 +69,16 @@ class LuckyNumberWidgetProvider : AppWidgetProvider() { PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE ) + if (luckyNumber is Resource.Error) { + Timber.e("Error loading lucky number for widget", luckyNumber.error) + } + val remoteView = RemoteViews(context.packageName, getCorrectLayoutId(appWidgetId, context)) .apply { setTextViewText( R.id.luckyNumberWidgetNumber, - luckyNumber?.luckyNumber?.toString() ?: "#" + luckyNumber.dataOrNull?.luckyNumber?.toString() ?: "#" ) setOnClickPendingIntent(R.id.luckyNumberWidgetContainer, appIntent) } @@ -167,14 +174,17 @@ class LuckyNumberWidgetProvider : AppWidgetProvider() { else -> null } - currentStudent?.let { - luckyNumberRepository.getLuckyNumber(it, false).toFirstResult().data + if (currentStudent != null) { + luckyNumberRepository.getLuckyNumber(currentStudent, forceRefresh = false) + .toFirstResult() + } else { + Resource.Success(null) } } catch (e: Exception) { if (e.cause !is NoCurrentStudentException) { Timber.e(e, "An error has occurred in lucky number provider") } - null + Resource.Error(e) } } 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 d81abe345..5cd6fa103 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 @@ -12,6 +12,7 @@ import androidx.fragment.app.DialogFragment import androidx.fragment.app.Fragment import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat +import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.elevation.ElevationOverlayProvider import com.ncapdevi.fragnav.FragNavController import com.ncapdevi.fragnav.FragNavController.Companion.HIDE @@ -20,19 +21,13 @@ import io.github.wulkanowy.R 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.databinding.DialogAdsConsentBinding 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.utils.AnalyticsHelper -import io.github.wulkanowy.utils.AppInfo -import io.github.wulkanowy.utils.InAppReviewHelper -import io.github.wulkanowy.utils.UpdateHelper -import io.github.wulkanowy.utils.createNameInitialsDrawable -import io.github.wulkanowy.utils.dpToPx -import io.github.wulkanowy.utils.getThemeAttrColor -import io.github.wulkanowy.utils.nickOrName -import io.github.wulkanowy.utils.safelyPopFragments -import io.github.wulkanowy.utils.setOnViewChangeListener +import io.github.wulkanowy.utils.* +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json import timber.log.Timber import javax.inject.Inject @@ -64,13 +59,13 @@ class MainActivity : BaseActivity(), MainVie companion object { - private const val EXTRA_START_DESTINATION = "start_destination" + private const val EXTRA_START_DESTINATION = "start_destination_json" fun getStartIntent( context: Context, destination: Destination? = null, ) = Intent(context, MainActivity::class.java).apply { - putExtra(EXTRA_START_DESTINATION, destination) + destination?.let { putExtra(EXTRA_START_DESTINATION, Json.encodeToString(it)) } } } @@ -79,9 +74,8 @@ class MainActivity : BaseActivity(), MainVie override val currentStackSize get() = navController.currentStack?.size override val currentViewTitle - get() = (navController.currentFrag as? MainView.TitledView)?.titleStringId?.let { - getString(it) - } + get() = (navController.currentFrag as? MainView.TitledView)?.titleStringId + ?.let { getString(it) } override val currentViewSubtitle get() = (navController.currentFrag as? MainView.TitledView)?.subtitleString @@ -95,7 +89,7 @@ class MainActivity : BaseActivity(), MainVie messageContainer = binding.mainMessageContainer updateHelper.messageContainer = binding.mainFragmentContainer - val destination = (intent.getSerializableExtra(EXTRA_START_DESTINATION) as Destination?) + val destination = intent.getStringExtra(EXTRA_START_DESTINATION) ?.takeIf { savedInstanceState == null } presenter.onAttachView(this, destination) @@ -108,6 +102,7 @@ class MainActivity : BaseActivity(), MainVie } //https://developer.android.com/guide/playcore/in-app-updates#status_callback + @Deprecated("Deprecated in Java") @Suppress("DEPRECATION") override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) @@ -138,7 +133,7 @@ class MainActivity : BaseActivity(), MainVie ) } fragmentHideStrategy = HIDE - rootFragments = rootDestinations.map { it.fragment } + rootFragments = rootDestinations.map { it.destinationFragment } initialize(startMenuIndex, savedInstanceState) } @@ -169,8 +164,12 @@ class MainActivity : BaseActivity(), MainVie .setIcon(R.drawable.ic_main_more) } selectedItemId = startMenuIndex - setOnItemSelectedListener { presenter.onTabSelected(it.itemId, false) } - setOnItemReselectedListener { presenter.onTabSelected(it.itemId, true) } + setOnItemSelectedListener { + this@MainActivity.presenter.onTabSelected(it.itemId, false) + } + setOnItemReselectedListener { + this@MainActivity.presenter.onTabSelected(it.itemId, true) + } } } @@ -178,8 +177,10 @@ class MainActivity : BaseActivity(), MainVie caller: PreferenceFragmentCompat, pref: Preference ): Boolean { - val fragment = - supportFragmentManager.fragmentFactory.instantiate(classLoader, pref.fragment) + val fragment = supportFragmentManager.fragmentFactory.instantiate( + classLoader, + pref.fragment.toString() + ) pushView(fragment) return true } @@ -233,7 +234,7 @@ class MainActivity : BaseActivity(), MainVie } override fun openMoreDestination(destination: Destination) { - pushView(destination.fragment) + pushView(destination.destinationFragment) } override fun notifyMenuViewReselected() { @@ -289,6 +290,50 @@ class MainActivity : BaseActivity(), MainVie inAppReviewHelper.showInAppReview(this) } + override fun showAppSupport() { + MaterialAlertDialogBuilder(this) + .setTitle(R.string.main_support_title) + .setMessage(R.string.main_support_description) + .setPositiveButton(R.string.main_support_positive) { _, _ -> presenter.onEnableAdsSelected() } + .setNegativeButton(android.R.string.cancel) { _, _ -> } + .setOnDismissListener { } + .show() + } + + override fun showPrivacyPolicyDialog() { + val dialogAdsConsentBinding = DialogAdsConsentBinding.inflate(layoutInflater) + + val dialog = MaterialAlertDialogBuilder(this) + .setTitle(R.string.pref_ads_consent_title) + .setMessage(R.string.pref_ads_consent_description) + .setView(dialogAdsConsentBinding.root) + .show() + + dialogAdsConsentBinding.adsConsentOver.setOnCheckedChangeListener { _, isChecked -> + dialogAdsConsentBinding.adsConsentPersonalised.isEnabled = isChecked + } + + dialogAdsConsentBinding.adsConsentPersonalised.setOnClickListener { + presenter.onPrivacyAgree(true) + dialog.dismiss() + } + + dialogAdsConsentBinding.adsConsentNonPersonalised.setOnClickListener { + presenter.onPrivacyAgree(false) + dialog.dismiss() + } + + dialogAdsConsentBinding.adsConsentPrivacy.setOnClickListener { presenter.onPrivacySelected() } + dialogAdsConsentBinding.adsConsentCancel.setOnClickListener { dialog.cancel() } + } + + override fun openPrivacyPolicy() { + openInternetBrowser( + "https://wulkanowy.github.io/polityka-prywatnosci.html", + ::showMessage + ) + } + override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) navController.onSaveInstanceState(outState) 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 c78931531..9c32d8583 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 @@ -1,9 +1,12 @@ package io.github.wulkanowy.ui.modules.main -import io.github.wulkanowy.data.Status import io.github.wulkanowy.data.db.entities.StudentWithSemesters +import io.github.wulkanowy.data.logResourceStatus +import io.github.wulkanowy.data.onResourceError +import io.github.wulkanowy.data.onResourceSuccess import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.StudentRepository +import io.github.wulkanowy.data.resourceFlow import io.github.wulkanowy.services.sync.SyncManager import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.BaseView @@ -15,19 +18,26 @@ 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.AdsHelper import io.github.wulkanowy.utils.AnalyticsHelper -import io.github.wulkanowy.utils.flowWithResource -import kotlinx.coroutines.flow.onEach +import io.github.wulkanowy.utils.AppInfo +import kotlinx.coroutines.launch +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json import timber.log.Timber -import java.time.LocalDate +import java.time.Duration +import java.time.Instant import javax.inject.Inject class MainPresenter @Inject constructor( errorHandler: ErrorHandler, studentRepository: StudentRepository, - private val prefRepository: PreferencesRepository, + private val preferencesRepository: PreferencesRepository, private val syncManager: SyncManager, private val analytics: AnalyticsHelper, + private val json: Json, + private val adsHelper: AdsHelper, + private val appInfo: AppInfo ) : BasePresenter(errorHandler, studentRepository) { private var studentsWitSemesters: List? = null @@ -42,19 +52,21 @@ class MainPresenter @Inject constructor( private val Destination?.startMenuIndex get() = when { - this == null -> prefRepository.startMenuIndex - type in rootDestinationTypeList -> { - rootDestinationTypeList.indexOf(type) + this == null -> preferencesRepository.startMenuIndex + destinationType in rootDestinationTypeList -> { + rootDestinationTypeList.indexOf(destinationType) } else -> 4 } - fun onAttachView(view: MainView, initDestination: Destination?) { + fun onAttachView(view: MainView, initDestinationJson: String?) { super.onAttachView(view) + val initDestination: Destination? = initDestinationJson?.let { json.decodeFromString(it) } + val startMenuIndex = initDestination.startMenuIndex val destinations = rootDestinationTypeList.map { - if (it == initDestination?.type) initDestination else it.defaultDestination + if (it == initDestination?.destinationType) initDestination else it.defaultDestination } view.initView(startMenuIndex, destinations) @@ -64,6 +76,8 @@ class MainPresenter @Inject constructor( syncManager.startPeriodicSyncWorker() + checkAppSupport() + analytics.logEvent("app_open", "destination" to initDestination.toString()) Timber.i("Main view was initialized with $initDestination") } @@ -74,20 +88,14 @@ class MainPresenter @Inject constructor( return } - flowWithResource { studentRepository.getSavedStudents(false) } - .onEach { resource -> - when (resource.status) { - Status.LOADING -> Timber.i("Loading student avatar data started") - Status.SUCCESS -> { - studentsWitSemesters = resource.data - showCurrentStudentAvatar() - } - Status.ERROR -> { - Timber.i("Loading student avatar result: An exception occurred") - errorHandler.dispatch(resource.error!!) - } - } - }.launch("avatar") + resourceFlow { studentRepository.getSavedStudents(false) } + .logResourceStatus("load student avatar") + .onResourceSuccess { + studentsWitSemesters = it + showCurrentStudentAvatar() + } + .onResourceError(errorHandler::dispatch) + .launch("avatar") } fun onViewChange(destinationView: BaseView) { @@ -154,18 +162,52 @@ class MainPresenter @Inject constructor( } == true } - private fun checkInAppReview() { - prefRepository.inAppReviewCount++ + fun onEnableAdsSelected() { + view?.showPrivacyPolicyDialog() + } - if (prefRepository.inAppReviewDate == null) { - prefRepository.inAppReviewDate = LocalDate.now() + fun onPrivacyAgree(isPersonalizedAds: Boolean) { + preferencesRepository.isAgreeToProcessData = true + preferencesRepository.isPersonalizedAdsEnabled = isPersonalizedAds + + adsHelper.initialize() + + preferencesRepository.isAdsEnabled = true + } + + fun onPrivacySelected() { + view?.openPrivacyPolicy() + } + + private fun checkInAppReview() { + preferencesRepository.inAppReviewCount++ + + if (preferencesRepository.inAppReviewDate == null) { + preferencesRepository.inAppReviewDate = Instant.now() } - if (!prefRepository.isAppReviewDone && prefRepository.inAppReviewCount >= 50 && - LocalDate.now().minusDays(14).isAfter(prefRepository.inAppReviewDate) + if (!preferencesRepository.isAppReviewDone && preferencesRepository.inAppReviewCount >= 50 && + Instant.now().minus(Duration.ofDays(14)).isAfter(preferencesRepository.inAppReviewDate) ) { view?.showInAppReview() - prefRepository.isAppReviewDone = true + preferencesRepository.isAppReviewDone = true + } + } + + private fun checkAppSupport() { + if (!preferencesRepository.isAppSupportShown && !preferencesRepository.isAdsEnabled + && appInfo.buildFlavor == "play" + ) { + presenterScope.launch { + val student = runCatching { studentRepository.getCurrentStudent(false) } + .onFailure { Timber.e(it) } + .getOrElse { return@launch } + + if (Instant.now().minus(Duration.ofDays(28)).isAfter(student.registrationDate)) { + view?.showAppSupport() + preferencesRepository.isAppSupportShown = true + } + } } } 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 3a57fcc6b..3d018e3d6 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 @@ -41,6 +41,12 @@ interface MainView : BaseView { fun showInAppReview() + fun showAppSupport() + + fun showPrivacyPolicyDialog() + + fun openPrivacyPolicy() + fun openMoreDestination(destination: Destination) interface MainChildView { 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 acf3133d9..4607793c9 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,12 +4,14 @@ import android.os.Bundle import android.view.View import android.view.View.INVISIBLE import android.view.View.VISIBLE +import android.view.ViewGroup +import androidx.core.view.isVisible +import androidx.core.view.updateLayoutParams +import androidx.core.view.updateMargins 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 -import io.github.wulkanowy.data.enums.MessageFolder.SENT -import io.github.wulkanowy.data.enums.MessageFolder.TRASHED +import io.github.wulkanowy.data.enums.MessageFolder.* import io.github.wulkanowy.databinding.FragmentMessageBinding import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.base.BaseFragmentPagerAdapter @@ -78,7 +80,6 @@ class MessageFragment : BaseFragment(R.layout.fragment_m } binding.messageTabLayout.elevation = requireContext().dpToPx(4f) - binding.openSendMessageButton.setOnClickListener { presenter.onSendMessageButtonClicked() } } @@ -93,12 +94,37 @@ class MessageFragment : BaseFragment(R.layout.fragment_m binding.messageProgress.visibility = if (show) VISIBLE else INVISIBLE } + override fun showNewMessage(show: Boolean) { + binding.openSendMessageButton.run { + if (show) show() else hide() + } + } + + override fun showTabLayout(show: Boolean) { + binding.messageTabLayout.isVisible = show + + with(binding.messageViewPager) { + isUserInputEnabled = show + updateLayoutParams { + updateMargins(top = if (show) requireContext().dpToPx(48f).toInt() else 0) + } + } + } + + fun onChildFragmentShowActionMode(show: Boolean) { + presenter.onChildViewShowActionMode(show) + } + fun onChildFragmentLoaded() { presenter.onChildViewLoaded() } - override fun notifyChildMessageDeleted(tabId: Int) { - (pagerAdapter.getFragmentInstance(tabId) as? MessageTabFragment)?.onParentDeleteMessage() + fun onChildFragmentShowNewMessage(show: Boolean) { + presenter.onChildViewShowNewMessage(show) + } + + fun onFragmentChanged() { + presenter.onFragmentChanged() } override fun notifyChildLoadData(index: Int, forceRefresh: Boolean) { @@ -106,6 +132,13 @@ class MessageFragment : BaseFragment(R.layout.fragment_m ?.onParentLoadData(forceRefresh) } + override fun notifyChildrenFinishActionMode() { + repeat(3) { + (pagerAdapter.getFragmentInstance(it) as? MessageTabFragment) + ?.onParentFinishActionMode() + } + } + override fun openSendMessage() { context?.let { it.startActivity(SendMessageActivity.getStartIntent(it)) } } 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 9e19517bd..68bdc4b7c 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 @@ -3,7 +3,6 @@ package io.github.wulkanowy.ui.modules.message import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.ErrorHandler -import kotlinx.coroutines.delay import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject @@ -24,6 +23,7 @@ class MessagePresenter @Inject constructor( fun onPageSelected(index: Int) { loadChild(index) + view?.notifyChildrenFinishActionMode() } private fun loadData() { @@ -35,6 +35,10 @@ class MessagePresenter @Inject constructor( view?.notifyChildLoadData(index, forceRefresh) } + fun onFragmentChanged() { + view?.notifyChildrenFinishActionMode() + } + fun onChildViewLoaded() { view?.apply { showContent(true) @@ -42,6 +46,14 @@ class MessagePresenter @Inject constructor( } } + fun onChildViewShowNewMessage(show: Boolean) { + view?.showNewMessage(show) + } + + fun onChildViewShowActionMode(show: Boolean) { + view?.showTabLayout(!show) + } + fun onSendMessageButtonClicked() { view?.openSendMessage() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessageView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessageView.kt index 2aa4d78ec..e0cc5098c 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessageView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessageView.kt @@ -12,9 +12,13 @@ interface MessageView : BaseView { fun showProgress(show: Boolean) + fun showNewMessage(show: Boolean) + + fun showTabLayout(show: Boolean) + fun notifyChildLoadData(index: Int, forceRefresh: Boolean) - fun notifyChildMessageDeleted(tabId: Int) + fun notifyChildrenFinishActionMode() fun openSendMessage() } 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 d75128be1..3c1c53d39 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 @@ -4,6 +4,8 @@ import android.annotation.SuppressLint import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.core.text.HtmlCompat.FROM_HTML_MODE_COMPACT +import androidx.core.text.parseAsHtml import androidx.recyclerview.widget.RecyclerView import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Message @@ -75,29 +77,25 @@ class MessagePreviewAdapter @Inject constructor() : @SuppressLint("SetTextI18n") private fun bindMessage(holder: MessageViewHolder, message: Message) { val context = holder.binding.root.context - val recipientCount = message.unreadBy + message.readBy - val readText = when { - recipientCount > 1 -> { - 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)) - } - else -> context.getString(R.string.message_read, context.getString(R.string.all_no)) + val readTextValue = when { + !message.unread -> R.string.all_yes + else -> R.string.all_no } + val readText = context.getString(R.string.message_read, context.getString(readTextValue)) with(holder.binding) { - messagePreviewSubject.text = - message.subject.ifBlank { root.context.getString(R.string.message_no_subject) } - messagePreviewDate.text = root.context.getString( + messagePreviewSubject.text = message.subject.ifBlank { + context.getString(R.string.message_no_subject) + } + messagePreviewDate.text = context.getString( R.string.message_date, message.date.toFormattedString("yyyy-MM-dd HH:mm:ss") ) messagePreviewRead.text = readText - messagePreviewContent.text = message.content + messagePreviewContent.text = message.content.parseAsHtml(FROM_HTML_MODE_COMPACT) messagePreviewFromSender.text = message.sender - messagePreviewToRecipient.text = message.recipient + messagePreviewToRecipient.text = message.recipients } } 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 e1cc2e374..2a5523f4d 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 @@ -57,7 +57,8 @@ class MessagePreviewFragment : get() = getString(R.string.message_no_subject) override val printHTML: String - get() = requireContext().assets.open("message-print-page.html").bufferedReader().use { it.readText() } + get() = requireContext().assets.open("message-print-page.html").bufferedReader() + .use { it.readText() } override val messageNotExists: String get() = getString(R.string.message_not_exists) @@ -81,7 +82,10 @@ class MessagePreviewFragment : super.onViewCreated(view, savedInstanceState) binding = FragmentMessagePreviewBinding.bind(view) messageContainer = binding.messagePreviewContainer - presenter.onAttachView(this, (savedInstanceState ?: arguments)?.getSerializable(MESSAGE_ID_KEY) as? Message) + presenter.onAttachView( + this, + (savedInstanceState ?: arguments)?.getSerializable(MESSAGE_ID_KEY) as? Message + ) } override fun initView() { @@ -101,6 +105,8 @@ class MessagePreviewFragment : menuShareButton = menu.findItem(R.id.messagePreviewMenuShare) menuPrintButton = menu.findItem(R.id.messagePreviewMenuPrint) presenter.onCreateOptionsMenu() + + menu.findItem(R.id.mainMenuAccount).isVisible = false } override fun onOptionsItemSelected(item: MenuItem): Boolean { @@ -129,8 +135,8 @@ class MessagePreviewFragment : binding.messagePreviewRecycler.visibility = if (show) VISIBLE else GONE } - override fun showOptions(show: Boolean) { - menuReplyButton?.isVisible = show + override fun showOptions(show: Boolean, isReplayable: Boolean) { + menuReplyButton?.isVisible = isReplayable menuForwardButton?.isVisible = show menuDeleteButton?.isVisible = show menuShareButton?.isVisible = show @@ -142,7 +148,7 @@ class MessagePreviewFragment : } override fun setNotDeletedOptionsLabels() { - menuDeleteButton?.setTitle(R.string.message_move_to_bin) + menuDeleteButton?.setTitle(R.string.message_move_to_trash) } override fun showErrorView(show: Boolean) { @@ -173,7 +179,8 @@ class MessagePreviewFragment : val webView = WebView(requireContext()) webView.webViewClient = object : WebViewClient() { - override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest) = false + override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest) = + false override fun onPageFinished(view: WebView, url: String) { createWebPrintJob(view, 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 eb33ee6ea..c011f41f3 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,20 +1,19 @@ package io.github.wulkanowy.ui.modules.message.preview import android.annotation.SuppressLint -import io.github.wulkanowy.data.Status +import androidx.core.text.parseAsHtml +import io.github.wulkanowy.data.* import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.db.entities.MessageAttachment import io.github.wulkanowy.data.enums.MessageFolder +import io.github.wulkanowy.data.repositories.MailboxRepository import io.github.wulkanowy.data.repositories.MessageRepository 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.afterLoading -import io.github.wulkanowy.utils.flowWithResource -import io.github.wulkanowy.utils.flowWithResourceIn import io.github.wulkanowy.utils.toFormattedString -import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject @@ -22,6 +21,7 @@ class MessagePreviewPresenter @Inject constructor( errorHandler: ErrorHandler, studentRepository: StudentRepository, private val messageRepository: MessageRepository, + private val mailboxRepository: MailboxRepository, private val analytics: AnalyticsHelper ) : BasePresenter(errorHandler, studentRepository) { @@ -53,44 +53,43 @@ class MessagePreviewPresenter @Inject constructor( view?.showErrorDetailsDialog(lastError) } - private fun loadData(message: Message) { - flowWithResourceIn { - val student = studentRepository.getStudentById(message.studentId) - messageRepository.getMessage(student, message, true) - }.onEach { - when (it.status) { - Status.LOADING -> Timber.i("Loading message ${message.messageId} preview started") - Status.SUCCESS -> { - Timber.i("Loading message ${message.messageId} preview result: Success ") - if (it.data != null) { - this@MessagePreviewPresenter.message = it.data.message - this@MessagePreviewPresenter.attachments = it.data.attachments - view?.apply { - setMessageWithAttachment(it.data) - showContent(true) - initOptions() - } - analytics.logEvent( - "load_item", - "type" to "message_preview", - "length" to it.data.message.content.length - ) - } else { - view?.run { - showMessage(messageNotExists) - popView() - } + private fun loadData(messageToLoad: Message) { + flatResourceFlow { + val student = studentRepository.getCurrentStudent() + messageRepository.getMessage(student, messageToLoad, true) + } + .logResourceStatus("message ${messageToLoad.messageId} preview") + .onResourceData { + if (it != null) { + message = it.message + attachments = it.attachments + view?.apply { + setMessageWithAttachment(it) + showContent(true) + initOptions() + } + } else { + view?.run { + showMessage(messageNotExists) + popView() } } - Status.ERROR -> { - Timber.i("Loading message ${message.messageId} preview result: An exception occurred ") - retryCallback = { onMessageLoadRetry(message) } - errorHandler.dispatch(it.error!!) + } + .onResourceSuccess { + if (it != null) { + analytics.logEvent( + "load_item", + "type" to "message_preview", + "length" to it.message.content.length + ) } } - }.afterLoading { - view?.showProgress(false) - }.launch() + .onResourceNotLoading { view?.showProgress(false) } + .onResourceError { + retryCallback = { onMessageLoadRetry(messageToLoad) } + errorHandler.dispatch(it) + } + .launch() } fun onReply(): Boolean { @@ -108,62 +107,69 @@ 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}" + val message = message ?: return false + val subject = message.subject.ifBlank { view?.messageNoSubjectString.orEmpty() } - attachments?.let { attachments -> - if (attachments.isNotEmpty()) { - text += "\n\nZałączniki:" + val text = buildString { + appendLine("Temat: $subject") + appendLine("Od: ${message.sender}") + appendLine("Do: ${message.recipients}") + appendLine("Data: ${message.date.toFormattedString("yyyy-MM-dd HH:mm:ss")}") - attachments.forEach { attachment -> - text += "\n${attachment.filename}: ${attachment.url}" - } - } + appendLine() + + appendLine(message.content.parseAsHtml()) + + if (!attachments.isNullOrEmpty()) { + appendLine() + appendLine("Załączniki:") + + append(attachments.orEmpty().joinToString(separator = "\n") { attachment -> + "${attachment.filename}: ${attachment.url}" + }) } - - view?.shareText( - text, - "FW: ${it.subject.ifBlank { view?.messageNoSubjectString.orEmpty() }}" - ) - return true } - return false + + view?.shareText( + subject = "FW: $subject", + text = text, + ) + return true } @SuppressLint("NewApi") fun onPrint(): Boolean { - message?.let { - val dateString = it.date.toFormattedString("yyyy-MM-dd HH:mm:ss") - val infoContent = "

Data wysłania

$dateString
" + when { - it.sender.isNotEmpty() -> "

Od

${it.sender}
" - else -> "

Do

${it.recipient}
" - } + val message = message ?: return false + val subject = message.subject.ifBlank { view?.messageNoSubjectString.orEmpty() } - val messageContent = "

${it.content}

" - .replace(Regex("[\\n\\r]{2,}"), "

") - .replace(Regex("[\\n\\r]"), "
") + val dateString = message.date.toFormattedString("yyyy-MM-dd HH:mm:ss") - val jobName = "Wiadomość " + when { - it.sender.isNotEmpty() -> "od ${it.sender}" - else -> "do ${it.recipient}" - } + " $dateString: ${it.subject.ifBlank { view?.messageNoSubjectString.orEmpty() }} | Wulkanowy" + val infoContent = buildString { + append("

Data wysłania

$dateString
") - view?.apply { - val html = printHTML - .replace( - "%SUBJECT%", - it.subject.ifBlank { view?.messageNoSubjectString.orEmpty() }) - .replace("%CONTENT%", messageContent) - .replace("%INFO%", infoContent) - printDocument(html, jobName) - } - return true + append("

Od

${message.sender}
") + append("

DO

${message.recipients}
") } - return false + val messageContent = "

${message.content}

" + .replace(Regex("[\\n\\r]{2,}"), "

") + .replace(Regex("[\\n\\r]"), "
") + + val jobName = buildString { + append("Wiadomość ") + append("od ${message.correspondents}") + append("do ${message.correspondents}") + append(" $dateString: $subject | Wulkanowy") + } + + view?.apply { + val html = printHTML + .replace("%SUBJECT%", subject) + .replace("%CONTENT%", messageContent) + .replace("%INFO%", infoContent) + printDocument(html, jobName) + } + + return true } private fun deleteMessage() { @@ -172,32 +178,31 @@ class MessagePreviewPresenter @Inject constructor( view?.run { showContent(false) showProgress(true) - showOptions(false) + showOptions(show = false, isReplayable = false) showErrorView(false) } - flowWithResource { - val student = studentRepository.getCurrentStudent() - messageRepository.deleteMessage(student, message!!) - }.onEach { - when (it.status) { - Status.LOADING -> Timber.d("Message ${message?.id} delete started") - Status.SUCCESS -> { - Timber.d("Message ${message?.id} delete success") + Timber.i("Delete message ${message?.messageGlobalKey}") + + presenterScope.launch { + runCatching { + val student = studentRepository.getCurrentStudent(decryptPass = true) + val mailbox = mailboxRepository.getMailbox(student) + messageRepository.deleteMessage(student, mailbox, message!!) + } + .onFailure { + retryCallback = { onMessageDelete() } + errorHandler.dispatch(it) + } + .onSuccess { view?.run { showMessage(deleteMessageSuccessString) popView() } } - Status.ERROR -> { - Timber.d("Message ${message?.id} delete failed") - retryCallback = { onMessageDelete() } - errorHandler.dispatch(it.error!!) - } - } - }.afterLoading { + view?.showProgress(false) - }.launch("delete") + } } private fun showErrorViewOnError(message: String, error: Throwable) { @@ -217,7 +222,10 @@ class MessagePreviewPresenter @Inject constructor( private fun initOptions() { view?.apply { - showOptions(message != null) + showOptions( + show = message != null, + isReplayable = message?.folderId != MessageFolder.SENT.id, + ) message?.let { when (it.folderId == MessageFolder.TRASHED.id) { true -> setDeletedOptionsLabels() 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 88fe77d94..c5a947939 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 @@ -28,7 +28,7 @@ interface MessagePreviewView : BaseView { fun setErrorRetryCallback(callback: () -> Unit) - fun showOptions(show: Boolean) + fun showOptions(show: Boolean, isReplayable: Boolean) fun setDeletedOptionsLabels() 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 70f9a9b54..334e389e2 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 @@ -6,6 +6,7 @@ import android.content.Context import android.content.Intent import android.graphics.Rect import android.os.Bundle +import android.text.Spanned import android.view.Menu import android.view.MenuItem import android.view.TouchDelegate @@ -13,11 +14,12 @@ import android.view.View.GONE import android.view.View.VISIBLE import android.widget.Toast import android.widget.Toast.LENGTH_LONG +import androidx.core.text.parseAsHtml +import androidx.core.text.toHtml import androidx.core.widget.doOnTextChanged import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Message -import io.github.wulkanowy.data.db.entities.ReportingUnit import io.github.wulkanowy.databinding.ActivitySendMessageBinding import io.github.wulkanowy.ui.base.BaseActivity import io.github.wulkanowy.utils.dpToPx @@ -72,17 +74,32 @@ class SendMessageActivity : BaseActivity formSubjectValue = binding.sendMessageSubject.text.toString() - formContentValue = binding.sendMessageMessageContent.text.toString() + formContentValue = + binding.sendMessageMessageContent.text.toString().parseAsHtml().toString() presenter.onAttachView( view = this, @@ -110,7 +127,7 @@ class SendMessageActivity : BaseActivity) { @@ -165,7 +182,7 @@ class SendMessageActivity : BaseActivity "Re: " - else -> "FW: " - } + message.subject) + setSubject( + when (reply) { + true -> "RE: " + else -> "FW: " + } + message.subject + ) if (preferencesRepository.fillMessageContent || reply != true) { - setContent( - when (reply) { - true -> "\n\n" - else -> "" - } + when (message.sender.isNotEmpty()) { - true -> "Od: ${message.sender}\n" - false -> "Do: ${message.recipient}\n" - } + "Data: ${message.date.toFormattedString("yyyy-MM-dd HH:mm:ss")}\n\n${message.content}") + setContent(buildString { + if (reply == true) { + append("

") + } + + append("Od: ${message.sender}
") + append("Do: ${message.recipients}
") + append("Data: ${message.date.toFormattedString("yyyy-MM-dd HH:mm:ss")}

") + append(message.content) + }) } } } @@ -111,72 +111,83 @@ class SendMessagePresenter @Inject constructor( } private fun loadData(message: Message?, reply: Boolean?) { - flowWithResource { + resourceFlow { val student = studentRepository.getCurrentStudent() - val semester = semesterRepository.getCurrentSemester(student) - val unit = reportingUnitRepository.getReportingUnit(student, semester.unitId) + val mailbox = mailboxRepository.getMailbox(student) Timber.i("Loading recipients started") - val recipients = when { - unit != null -> recipientRepository.getRecipients(student, unit, 2) - else -> listOf() - }.let { createChips(it) } + val recipients = createChips( + recipients = recipientRepository.getRecipients( + student = student, + mailbox = mailbox, + type = MailboxType.EMPLOYEE, + ) + ) Timber.i("Loading recipients result: Success, fetched %d recipients", recipients.size) Timber.i("Loading message recipients started") val messageRecipients = when { - message != null && reply == true -> recipientRepository.getMessageRecipients(student, message) + message != null && reply == true -> recipientRepository.getMessageSender( + student = student, + message = message, + mailbox = mailbox, + ) else -> emptyList() }.let { createChips(it) } - Timber.i("Loaded message recipients to reply result: Success, fetched %d recipients", messageRecipients.size) + Timber.i( + "Loaded message recipients to reply result: Success, fetched %d recipients", + messageRecipients.size + ) - Triple(unit, recipients, messageRecipients) - }.onEach { - when (it.status) { - Status.LOADING -> view?.run { - Timber.i("Loading recipients started") - showProgress(true) - showContent(false) - } - Status.SUCCESS -> it.data!!.let { (reportingUnit, recipientChips, selectedRecipientChips) -> - view?.run { - if (reportingUnit != null) { - setReportingUnit(reportingUnit) + Triple(mailbox, recipients, messageRecipients) + } + .logResourceStatus("load recipients") + .onEach { + when (it) { + is Resource.Loading -> view?.run { + showProgress(true) + showContent(false) + } + is Resource.Success -> it.data.let { (mailbox, recipientChips, selectedRecipientChips) -> + view?.run { + setMailbox(getMailboxName(mailbox)) setRecipients(recipientChips) - if (selectedRecipientChips.isNotEmpty()) setSelectedRecipients(selectedRecipientChips) + if (selectedRecipientChips.isNotEmpty()) setSelectedRecipients( + selectedRecipientChips + ) showContent(true) - } else { - Timber.i("Loading recipients result: Can't find the reporting unit") - view?.showEmpty(true) } } + is Resource.Error -> { + view?.showContent(true) + errorHandler.dispatch(it.error) + } } - Status.ERROR -> { - Timber.i("Loading recipients result: An exception occurred") - view?.showContent(true) - errorHandler.dispatch(it.error!!) - } - } - }.afterLoading { - view?.run { showProgress(false) } - }.launch() + }.onResourceNotLoading { + view?.run { showProgress(false) } + }.launch() } private fun sendMessage(subject: String, content: String, recipients: List) { - flowWithResource { + resourceFlow { val student = studentRepository.getCurrentStudent() - messageRepository.sendMessage(student, subject, content, recipients) - }.onEach { - when (it.status) { - Status.LOADING -> view?.run { - Timber.i("Sending message started") + val mailbox = mailboxRepository.getMailbox(student) + messageRepository.sendMessage( + student = student, + subject = subject, + content = content, + recipients = recipients, + mailboxId = mailbox.globalKey, + ) + }.logResourceStatus("sending message").onEach { + when (it) { + is Resource.Loading -> view?.run { showSoftInput(false) showContent(false) showProgress(true) showActionBar(false) } - Status.SUCCESS -> { - Timber.i("Sending message result: Success") + is Resource.Success -> { view?.clearDraft() view?.run { showMessage(messageSuccess) @@ -184,45 +195,57 @@ class SendMessagePresenter @Inject constructor( } analytics.logEvent("send_message", "recipients" to recipients.size) } - Status.ERROR -> { - Timber.i("Sending message result: An exception occurred") + is Resource.Error -> { view?.run { showContent(true) showProgress(false) showActionBar(true) } - errorHandler.dispatch(it.error!!) + errorHandler.dispatch(it.error) } } }.launch("send") } private fun createChips(recipients: List): List { - fun generateCorrectSummary(recipientRealName: String): String { - val substring = recipientRealName.substringBeforeLast("-") - return when { - substring == recipientRealName -> recipientRealName - substring.indexOf("(") != -1 -> { - recipientRealName.indexOf("(") - .let { recipientRealName.substring(if (it != -1) it else 0) } - } - substring.indexOf("[") != -1 -> { - recipientRealName.indexOf("[") - .let { recipientRealName.substring(if (it != -1) it else 0) } - } - else -> recipientRealName.substringAfter("-") - }.trim() - } - return recipients.map { RecipientChipItem( - title = it.name, - summary = generateCorrectSummary(it.realName), + title = it.userName, + summary = buildString { + getMailboxType(it.type)?.let(::append) + if (isNotBlank()) append(" ") + + append("(${it.schoolShortName})") + }, recipient = it ) } } + private fun getMailboxName(mailbox: Mailbox): String { + return buildString { + append(mailbox.userName) + append(" - ") + append(getMailboxType(mailbox.type)) + + if (mailbox.type == MailboxType.PARENT) { + append(" - ") + append(mailbox.studentName) + } + + append(" - ") + append("(${mailbox.schoolNameShort})") + } + } + + private fun getMailboxType(type: MailboxType): String? = when (type) { + MailboxType.STUDENT -> view?.mailboxStudent + MailboxType.PARENT -> view?.mailboxParent + MailboxType.GUARDIAN -> view?.mailboxGuardian + MailboxType.EMPLOYEE -> view?.mailboxEmployee + MailboxType.UNKNOWN -> null + } + fun onMessageContentChange() { presenterScope.launch { messageUpdateChannel.send(Unit) @@ -259,7 +282,8 @@ class SendMessagePresenter @Inject constructor( } fun getRecipientsNames(): String { - return messageRepository.draftMessage?.recipients.orEmpty().joinToString { it.recipient.name } + return messageRepository.draftMessage?.recipients.orEmpty() + .joinToString { it.recipient.userName } } fun clearDraft() { @@ -267,6 +291,7 @@ class SendMessagePresenter @Inject constructor( Timber.i("Draft cleared!") } - fun getMessageBackupContent(recipients: String) = if (recipients.isEmpty()) view?.getMessageBackupDialogString() + fun getMessageBackupContent(recipients: String) = + if (recipients.isEmpty()) view?.getMessageBackupDialogString() else view?.getMessageBackupDialogStringWithRecipients(recipients) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/SendMessageView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/SendMessageView.kt index 21b42e3e4..1057114b8 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/SendMessageView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/SendMessageView.kt @@ -1,6 +1,6 @@ package io.github.wulkanowy.ui.modules.message.send -import io.github.wulkanowy.data.db.entities.ReportingUnit +import io.github.wulkanowy.data.db.entities.Mailbox import io.github.wulkanowy.ui.base.BaseView interface SendMessageView : BaseView { @@ -18,9 +18,17 @@ interface SendMessageView : BaseView { val messageSuccess: String + val mailboxStudent: String + + val mailboxParent: String + + val mailboxGuardian: String + + val mailboxEmployee: String + fun initView() - fun setReportingUnit(unit: ReportingUnit) + fun setMailbox(mailbox: String) fun setRecipients(recipients: List) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabAdapter.kt index 571cc6d55..55f03ef84 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabAdapter.kt @@ -2,16 +2,12 @@ package io.github.wulkanowy.ui.modules.message.tab import android.graphics.Typeface import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup import android.widget.CompoundButton import androidx.core.view.isVisible import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView -import androidx.recyclerview.widget.RecyclerView.NO_POSITION import io.github.wulkanowy.R -import io.github.wulkanowy.data.db.entities.Message -import io.github.wulkanowy.data.enums.MessageFolder import io.github.wulkanowy.databinding.ItemMessageBinding import io.github.wulkanowy.databinding.ItemMessageChipsBinding import io.github.wulkanowy.utils.toFormattedString @@ -20,118 +16,137 @@ import javax.inject.Inject class MessageTabAdapter @Inject constructor() : RecyclerView.Adapter() { - enum class ViewType { HEADER, ITEM } + var onItemClickListener: (MessageTabDataItem.MessageItem, position: Int) -> Unit = { _, _ -> } - var onItemClickListener: (Message, position: Int) -> Unit = { _, _ -> } - var onHeaderClickListener: (chip: CompoundButton, isChecked: Boolean) -> Unit = { _, _ -> } + var onLongItemClickListener: (MessageTabDataItem.MessageItem) -> Unit = {} + + var onHeaderClickListener: (CompoundButton, Boolean) -> Unit = { _, _ -> } var onChangesDetectedListener = {} private var items = mutableListOf() - private var onlyUnread: Boolean? = null - private var onlyWithAttachments = false - fun setDataItems( - data: List, - onlyUnread: Boolean?, - onlyWithAttachments: Boolean - ) { - if (items.size != data.size) onChangesDetectedListener() + fun submitData(data: List) { + val originalMessagesSize = items.count { it.viewType == MessageItemViewType.MESSAGE } + val newMessagesSize = data.count { it.viewType == MessageItemViewType.MESSAGE } + + if (originalMessagesSize != newMessagesSize) onChangesDetectedListener() + val diffResult = DiffUtil.calculateDiff(MessageTabDiffUtil(items, data)) items = data.toMutableList() - this.onlyUnread = onlyUnread - this.onlyWithAttachments = onlyWithAttachments + diffResult.dispatchUpdatesTo(this) } - override fun getItemViewType(position: Int): Int { - return when (position) { - 0 -> ViewType.HEADER.ordinal - else -> ViewType.ITEM.ordinal - } - } + override fun getItemViewType(position: Int) = items[position].viewType.ordinal override fun getItemCount() = items.size override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { val inflater = LayoutInflater.from(parent.context) - return when (viewType) { - ViewType.ITEM.ordinal -> ItemViewHolder( + + return when (MessageItemViewType.values()[viewType]) { + MessageItemViewType.MESSAGE -> ItemViewHolder( ItemMessageBinding.inflate(inflater, parent, false) ) - ViewType.HEADER.ordinal -> HeaderViewHolder( + MessageItemViewType.FILTERS -> HeaderViewHolder( ItemMessageChipsBinding.inflate(inflater, parent, false) ) - else -> throw IllegalStateException() } } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { when (holder) { - is ItemViewHolder -> { - val item = (items[position] as MessageTabDataItem.MessageItem).message + is ItemViewHolder -> bindItemViewHolder(holder, position) + is HeaderViewHolder -> bindHeaderViewHolder(holder, position) + } + } - with(holder.binding) { - val style = if (item.unread) Typeface.BOLD else Typeface.NORMAL + private fun bindHeaderViewHolder(holder: HeaderViewHolder, position: Int) { + val item = items[position] as MessageTabDataItem.FilterHeader - messageItemAuthor.run { - text = - if (item.folderId == MessageFolder.SENT.id) item.recipient else item.sender - setTypeface(null, style) - } - messageItemSubject.run { - text = - if (item.subject.isNotBlank()) item.subject else context.getString(R.string.message_no_subject) - setTypeface(null, style) - } - messageItemDate.run { - text = item.date.toFormattedString() - setTypeface(null, style) - } - messageItemAttachmentIcon.visibility = - if (item.hasAttachments) View.VISIBLE else View.GONE + with(holder.binding) { + if (item.onlyUnread == null) { + chipUnread.isVisible = false + } else { + chipUnread.isVisible = true + chipUnread.isChecked = item.onlyUnread + chipUnread.setOnCheckedChangeListener(onHeaderClickListener) + } + chipUnread.isEnabled = item.isEnabled + chipAttachments.isEnabled = item.isEnabled + chipAttachments.isChecked = item.onlyWithAttachments + chipAttachments.setOnCheckedChangeListener(onHeaderClickListener) + } + } - root.setOnClickListener { - holder.bindingAdapterPosition.let { - if (it != NO_POSITION) onItemClickListener(item, it) - } + private fun bindItemViewHolder(holder: ItemViewHolder, position: Int) { + val item = (items[position] as MessageTabDataItem.MessageItem) + val message = item.message + + with(holder.binding) { + val style = if (message.unread) Typeface.BOLD else Typeface.NORMAL + + with(messageItemAuthor) { + text = message.correspondents + setTypeface(null, style) + } + messageItemSubject.run { + text = message.subject.ifBlank { context.getString(R.string.message_no_subject) } + setTypeface(null, style) + } + messageItemDate.run { + text = message.date.toFormattedString() + setTypeface(null, style) + } + messageItemAttachmentIcon.isVisible = message.hasAttachments + + root.setOnClickListener { + holder.bindingAdapterPosition.let { + if (it != RecyclerView.NO_POSITION) { + onItemClickListener(item, it) } } } - is HeaderViewHolder -> { - with(holder.binding) { - if (onlyUnread == null) chipUnread.isVisible = false - else { - chipUnread.isVisible = true - chipUnread.isChecked = onlyUnread!! - chipUnread.setOnCheckedChangeListener(onHeaderClickListener) - } - chipAttachments.isChecked = onlyWithAttachments - chipAttachments.setOnCheckedChangeListener(onHeaderClickListener) - } + + root.setOnLongClickListener { + onLongItemClickListener(item) + return@setOnLongClickListener true + } + + with(messageItemCheckbox) { + isChecked = item.isSelected + isVisible = item.isActionMode } } } class ItemViewHolder(val binding: ItemMessageBinding) : RecyclerView.ViewHolder(binding.root) + class HeaderViewHolder(val binding: ItemMessageChipsBinding) : RecyclerView.ViewHolder(binding.root) private class MessageTabDiffUtil( private val old: List, private val new: List - ) : - DiffUtil.Callback() { + ) : DiffUtil.Callback() { + override fun getOldListSize(): Int = old.size override fun getNewListSize(): Int = new.size override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { - return old[oldItemPosition].id == new[newItemPosition].id + val oldItem = old[oldItemPosition] + val newItem = new[newItemPosition] + + return if (oldItem is MessageTabDataItem.MessageItem && newItem is MessageTabDataItem.MessageItem) { + oldItem.message.messageGlobalKey == newItem.message.messageGlobalKey + } else { + oldItem.viewType == newItem.viewType + } } - override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { - return old[oldItemPosition] == new[newItemPosition] - } + override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) = + old[oldItemPosition] == new[newItemPosition] } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabDataItem.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabDataItem.kt index 4f51a936f..634dfc0e7 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabDataItem.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabDataItem.kt @@ -2,14 +2,19 @@ package io.github.wulkanowy.ui.modules.message.tab import io.github.wulkanowy.data.db.entities.Message -sealed class MessageTabDataItem { - data class MessageItem(val message: Message) : MessageTabDataItem() { - override val id = message.id - } +sealed class MessageTabDataItem(val viewType: MessageItemViewType) { - object Header : MessageTabDataItem() { - override val id = Long.MIN_VALUE - } + data class MessageItem( + val message: Message, + val isSelected: Boolean, + val isActionMode: Boolean + ) : MessageTabDataItem(MessageItemViewType.MESSAGE) - abstract val id: Long + data class FilterHeader( + val onlyUnread: Boolean?, + val onlyWithAttachments: Boolean, + val isEnabled: Boolean + ) : MessageTabDataItem(MessageItemViewType.FILTERS) } + +enum class MessageItemViewType { FILTERS, MESSAGE } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabFragment.kt index 54ee74eb1..654b0e226 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabFragment.kt @@ -3,12 +3,13 @@ package io.github.wulkanowy.ui.modules.message.tab import android.os.Bundle import android.view.Menu import android.view.MenuInflater +import android.view.MenuItem import android.view.View -import android.view.View.GONE -import android.view.View.INVISIBLE -import android.view.View.VISIBLE +import android.view.View.* import android.widget.CompoundButton +import androidx.appcompat.view.ActionMode import androidx.appcompat.widget.SearchView +import androidx.core.view.updatePadding import androidx.recyclerview.widget.LinearLayoutManager import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R @@ -20,7 +21,9 @@ import io.github.wulkanowy.ui.modules.main.MainActivity import io.github.wulkanowy.ui.modules.message.MessageFragment import io.github.wulkanowy.ui.modules.message.preview.MessagePreviewFragment import io.github.wulkanowy.ui.widgets.DividerItemDecoration +import io.github.wulkanowy.utils.dpToPx import io.github.wulkanowy.utils.getThemeAttrColor +import io.github.wulkanowy.utils.hideSoftInput import javax.inject.Inject @AndroidEntryPoint @@ -31,9 +34,10 @@ class MessageTabFragment : BaseFragment(R.layout.frag lateinit var presenter: MessageTabPresenter @Inject - lateinit var tabAdapter: MessageTabAdapter + lateinit var messageTabAdapter: MessageTabAdapter companion object { + const val MESSAGE_TAB_FOLDER_ID = "message_tab_folder_id" fun newInstance(folder: MessageFolder): MessageTabFragment { @@ -46,11 +50,38 @@ class MessageTabFragment : BaseFragment(R.layout.frag } override val isViewEmpty - get() = tabAdapter.itemCount == 0 + get() = messageTabAdapter.itemCount == 0 - override var onlyUnread: Boolean? = false + private var actionMode: ActionMode? = null - override var onlyWithAttachments = false + private val actionModeCallback = object : ActionMode.Callback { + override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { + val inflater = mode.menuInflater + inflater.inflate(R.menu.context_menu_message_tab, menu) + return true + } + + override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { + if (presenter.folder == MessageFolder.TRASHED) { + val menuItem = menu.findItem(R.id.messageTabContextMenuDelete) + menuItem.setTitle(R.string.message_delete_forever) + } + return presenter.onPrepareActionMode() + } + + override fun onDestroyActionMode(mode: ActionMode) { + presenter.onDestroyActionMode() + actionMode = null + } + + override fun onActionItemClicked(mode: ActionMode, menu: MenuItem): Boolean { + when (menu.itemId) { + R.id.messageTabContextMenuDelete -> presenter.onActionModeSelectDelete() + R.id.messageTabContextMenuSelectAll -> presenter.onActionModeSelectCheckAll() + } + return true + } + } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -69,24 +100,25 @@ class MessageTabFragment : BaseFragment(R.layout.frag } override fun initView() { - with(tabAdapter) { + with(messageTabAdapter) { onItemClickListener = presenter::onMessageItemSelected + onLongItemClickListener = presenter::onMessageItemLongSelected onHeaderClickListener = ::onChipChecked onChangesDetectedListener = ::resetListPosition } with(binding.messageTabRecycler) { layoutManager = LinearLayoutManager(context) - adapter = tabAdapter + adapter = messageTabAdapter addItemDecoration(DividerItemDecoration(context, false)) + itemAnimator = null } + with(binding) { messageTabSwipe.setOnRefreshListener(presenter::onSwipeRefresh) messageTabSwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary)) messageTabSwipe.setProgressBackgroundColorSchemeColor( - requireContext().getThemeAttrColor( - R.attr.colorSwipeRefresh - ) + requireContext().getThemeAttrColor(R.attr.colorSwipeRefresh) ) messageTabErrorRetry.setOnClickListener { presenter.onRetry() } messageTabErrorDetails.setOnClickListener { presenter.onDetailsClick() } @@ -109,9 +141,28 @@ class MessageTabFragment : BaseFragment(R.layout.frag }) } - override fun updateData(data: List, hide: Boolean) { - if (hide) onlyUnread = null - tabAdapter.setDataItems(data, onlyUnread, onlyWithAttachments) + override fun updateData(data: List) { + messageTabAdapter.submitData(data) + } + + override fun updateActionModeTitle(selectedMessagesSize: Int) { + actionMode?.title = resources.getQuantityString( + R.plurals.message_selected_messages_count, + selectedMessagesSize, + selectedMessagesSize + ) + } + + override fun updateSelectAllMenu(isAllSelected: Boolean) { + val menuItem = actionMode?.menu?.findItem(R.id.messageTabContextMenuSelectAll) ?: return + + if (isAllSelected) { + menuItem.setTitle(R.string.message_unselect_all) + menuItem.setIcon(R.drawable.ic_message_unselect_all) + } else { + menuItem.setTitle(R.string.message_select_all) + menuItem.setIcon(R.drawable.ic_message_select_all) + } } override fun showProgress(show: Boolean) { @@ -146,6 +197,14 @@ class MessageTabFragment : BaseFragment(R.layout.frag binding.messageTabSwipe.isRefreshing = show } + override fun showMessagesDeleted() { + showMessage(getString(R.string.message_messages_deleted)) + } + + override fun notifyParentShowNewMessage(show: Boolean) { + (parentFragment as? MessageFragment)?.onChildFragmentShowNewMessage(show) + } + override fun openMessage(message: Message) { (activity as? MainActivity)?.pushView(MessagePreviewFragment.newInstance(message)) } @@ -154,12 +213,16 @@ class MessageTabFragment : BaseFragment(R.layout.frag (parentFragment as? MessageFragment)?.onChildFragmentLoaded() } - fun onParentLoadData( - forceRefresh: Boolean, - onlyUnread: Boolean? = this.onlyUnread, - onlyWithAttachments: Boolean = this.onlyWithAttachments - ) { - presenter.onParentViewLoadData(forceRefresh, onlyUnread, onlyWithAttachments) + override fun notifyParentShowActionMode(show: Boolean) { + (parentFragment as? MessageFragment)?.onChildFragmentShowActionMode(show) + } + + fun onParentLoadData(forceRefresh: Boolean) { + presenter.onParentViewLoadData(forceRefresh) + } + + fun onParentFinishActionMode() { + presenter.onParentFinishActionMode() } private fun onChipChecked(chip: CompoundButton, isChecked: Boolean) { @@ -169,8 +232,22 @@ class MessageTabFragment : BaseFragment(R.layout.frag } } - fun onParentDeleteMessage() { - presenter.onDeleteMessage() + override fun showActionMode(show: Boolean) { + if (show) { + actionMode = (activity as MainActivity?)?.startSupportActionMode(actionModeCallback) + } else { + actionMode?.finish() + } + } + + override fun showRecyclerBottomPadding(show: Boolean) { + binding.messageTabRecycler.updatePadding( + bottom = if (show) requireContext().dpToPx(64f).toInt() else 0 + ) + } + + override fun hideKeyboard() { + activity?.hideSoftInput() } override fun onSaveInstanceState(outState: Bundle) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabPresenter.kt index f70a1bab7..54711a689 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabPresenter.kt @@ -1,25 +1,21 @@ package io.github.wulkanowy.ui.modules.message.tab -import io.github.wulkanowy.data.Status +import io.github.wulkanowy.data.* import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.enums.MessageFolder +import io.github.wulkanowy.data.repositories.MailboxRepository import io.github.wulkanowy.data.repositories.MessageRepository -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.AnalyticsHelper -import io.github.wulkanowy.utils.afterLoading -import io.github.wulkanowy.utils.flowWithResourceIn import io.github.wulkanowy.utils.toFormattedString import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.consumeAsFlow import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import me.xdrop.fuzzywuzzy.FuzzySearch import timber.log.Timber @@ -30,7 +26,7 @@ class MessageTabPresenter @Inject constructor( errorHandler: ErrorHandler, studentRepository: StudentRepository, private val messageRepository: MessageRepository, - private val semesterRepository: SemesterRepository, + private val mailboxRepository: MailboxRepository, private val analytics: AnalyticsHelper ) : BasePresenter(errorHandler, studentRepository) { @@ -44,6 +40,14 @@ class MessageTabPresenter @Inject constructor( private val searchChannel = Channel() + private val messagesToDelete = mutableSetOf() + + private var onlyUnread: Boolean? = false + + private var onlyWithAttachments = false + + private var isActionMode = false + fun onAttachView(view: MessageTabView, folder: MessageFolder) { super.onAttachView(view) view.initView() @@ -54,14 +58,14 @@ class MessageTabPresenter @Inject constructor( fun onSwipeRefresh() { Timber.i("Force refreshing the $folder message") - view?.run { onParentViewLoadData(true, onlyUnread, onlyWithAttachments) } + view?.run { loadData(true) } } fun onRetry() { view?.run { showErrorView(false) showProgress(true) - loadData(true, onlyUnread == true, onlyWithAttachments) + loadData(true) } } @@ -69,102 +73,182 @@ class MessageTabPresenter @Inject constructor( view?.showErrorDetailsDialog(lastError) } - fun onDeleteMessage() { - view?.run { loadData(true, onlyUnread == true, onlyWithAttachments) } + fun onParentViewLoadData(forceRefresh: Boolean) { + loadData(forceRefresh) } - fun onParentViewLoadData( - forceRefresh: Boolean, - onlyUnread: Boolean? = view?.onlyUnread, - onlyWithAttachments: Boolean = view?.onlyWithAttachments == true - ) { - loadData(forceRefresh, onlyUnread == true, onlyWithAttachments) + fun onParentFinishActionMode() { + view?.showActionMode(false) } - fun onMessageItemSelected(message: Message, position: Int) { - Timber.i("Select message ${message.id} item (position: $position)") - view?.openMessage(message) + fun onDestroyActionMode() { + isActionMode = false + messagesToDelete.clear() + updateDataInView() + + view?.run { + enableSwipe(true) + notifyParentShowNewMessage(true) + notifyParentShowActionMode(false) + showRecyclerBottomPadding(true) + } + } + + fun onPrepareActionMode(): Boolean { + isActionMode = true + messagesToDelete.clear() + updateDataInView() + + view?.apply { + enableSwipe(false) + notifyParentShowNewMessage(false) + notifyParentShowActionMode(true) + showRecyclerBottomPadding(false) + hideKeyboard() + } + return true + } + + fun onActionModeSelectDelete() { + Timber.i("Delete ${messagesToDelete.size} messages)") + val messageList = messagesToDelete.toList() + + presenterScope.launch { + view?.run { + showProgress(true) + showContent(false) + showActionMode(false) + } + + runCatching { + val student = studentRepository.getCurrentStudent(true) + val mailbox = mailboxRepository.getMailbox(student) + messageRepository.deleteMessages(student, mailbox, messageList) + } + .onFailure(errorHandler::dispatch) + .onSuccess { view?.showMessagesDeleted() } + } + } + + fun onActionModeSelectCheckAll() { + val messagesToSelect = getFilteredData() + val isAllSelected = messagesToDelete.containsAll(messagesToSelect) + + if (isAllSelected) { + messagesToDelete.clear() + view?.showActionMode(false) + } else { + messagesToDelete.addAll(messagesToSelect) + updateDataInView() + } + + view?.run { + updateSelectAllMenu(!isAllSelected) + updateActionModeTitle(messagesToDelete.size) + } + } + + fun onMessageItemLongSelected(messageItem: MessageTabDataItem.MessageItem) { + if (!isActionMode) { + view?.showActionMode(true) + + messagesToDelete.add(messageItem.message) + + view?.updateActionModeTitle(messagesToDelete.size) + updateDataInView() + } + } + + fun onMessageItemSelected(messageItem: MessageTabDataItem.MessageItem, position: Int) { + Timber.i("Select message ${messageItem.message.messageGlobalKey} item (position: $position)") + + if (!isActionMode) { + view?.run { + showActionMode(false) + openMessage(messageItem.message) + } + } else { + if (!messageItem.isSelected) { + messagesToDelete.add(messageItem.message) + } else { + messagesToDelete.remove(messageItem.message) + } + + if (messagesToDelete.isEmpty()) { + view?.showActionMode(false) + } + + val filteredData = getFilteredData() + + view?.run { + updateActionModeTitle(messagesToDelete.size) + updateSelectAllMenu(messagesToDelete.containsAll(filteredData)) + } + updateDataInView() + } } fun onUnreadFilterSelected(isChecked: Boolean) { view?.run { onlyUnread = isChecked - onParentViewLoadData(false, onlyUnread, onlyWithAttachments) + loadData(false) } } fun onAttachmentsFilterSelected(isChecked: Boolean) { view?.run { onlyWithAttachments = isChecked - onParentViewLoadData(false, onlyUnread, onlyWithAttachments) + loadData(false) } } - private fun loadData( - forceRefresh: Boolean, - onlyUnread: Boolean, - onlyWithAttachments: Boolean - ) { + private fun loadData(forceRefresh: Boolean) { Timber.i("Loading $folder message data started") - flowWithResourceIn { + flatResourceFlow { val student = studentRepository.getCurrentStudent() - val semester = semesterRepository.getCurrentSemester(student) - messageRepository.getMessages(student, semester, folder, forceRefresh) - }.onEach { - when (it.status) { - Status.LOADING -> { - if (!it.data.isNullOrEmpty()) { - view?.run { - enableSwipe(true) - showErrorView(false) - showRefresh(true) - showProgress(false) - showContent(true) - messages = it.data - val filteredData = getFilteredData( - lastSearchQuery, - onlyUnread, - onlyWithAttachments - ) - val messageItems = filteredData.map { message -> - MessageTabDataItem.MessageItem(message) - } - val messageItemsWithHeader = - listOf(MessageTabDataItem.Header) + messageItems + val mailbox = mailboxRepository.getMailbox(student) + messageRepository.getMessages(student, mailbox, folder, forceRefresh) + } + .logResourceStatus("load $folder message") + .onResourceData { + messages = it - updateData(messageItemsWithHeader, folder.id == MessageFolder.SENT.id) - notifyParentDataLoaded() - } - } + val filteredData = getFilteredData() + + view?.run { + enableSwipe(true) + showErrorView(false) + showProgress(false) + showContent(true) + showEmpty(filteredData.isEmpty()) } - Status.SUCCESS -> { - Timber.i("Loading $folder message result: Success") - messages = it.data!! - updateData(getFilteredData(lastSearchQuery, onlyUnread, onlyWithAttachments)) - analytics.logEvent( - "load_data", - "type" to "messages", - "items" to it.data.size, - "folder" to folder.name - ) - } - Status.ERROR -> { - Timber.i("Loading $folder message result: An exception occurred") - errorHandler.dispatch(it.error!!) + + updateDataInView() + } + .onResourceIntermediate { view?.showRefresh(true) } + .onResourceSuccess { + analytics.logEvent( + "load_data", + "type" to "messages", + "items" to it.size, + "folder" to folder.name + ) + } + .onResourceNotLoading { + view?.run { + showRefresh(false) + showProgress(false) + enableSwipe(true) + notifyParentDataLoaded() } } - }.afterLoading { - view?.run { - showRefresh(false) - showProgress(false) - enableSwipe(true) - notifyParentDataLoaded() + .onResourceError(errorHandler::dispatch) + .catch { + errorHandler.dispatch(it) + view?.notifyParentDataLoaded() } - }.catch { - errorHandler.dispatch(it) - view?.notifyParentDataLoaded() - }.launch() + .launch() } private fun showErrorViewOnError(message: String, error: Throwable) { @@ -192,65 +276,79 @@ class MessageTabPresenter @Inject constructor( .debounce(250) .map { query -> lastSearchQuery = query - val isOnlyUnread = view?.onlyUnread == true - val isOnlyWithAttachments = view?.onlyWithAttachments == true - getFilteredData(query, isOnlyUnread, isOnlyWithAttachments) + + getFilteredData() } .catch { Timber.e(it) } .collect { Timber.d("Applying filter. Full list: ${messages.size}, filtered: ${it.size}") - updateData(it) + + view?.run { + showEmpty(it.isEmpty()) + showContent(true) + showErrorView(false) + } + + updateDataInView() view?.resetListPosition() } } } - private fun getFilteredData( - query: String, - onlyUnread: Boolean = false, - onlyWithAttachments: Boolean = false - ): List { - if (query.trim().isEmpty()) { + private fun getFilteredData(): List { + if (lastSearchQuery.trim().isEmpty()) { val sortedMessages = messages.sortedByDescending { it.date } return when { - onlyUnread && onlyWithAttachments -> sortedMessages.filter { it.unread == onlyUnread && it.hasAttachments == onlyWithAttachments } - onlyUnread -> sortedMessages.filter { it.unread == onlyUnread } + (onlyUnread == true) && onlyWithAttachments -> sortedMessages.filter { it.unread == onlyUnread && it.hasAttachments == onlyWithAttachments } + (onlyUnread == true) -> sortedMessages.filter { it.unread == onlyUnread } onlyWithAttachments -> sortedMessages.filter { it.hasAttachments == onlyWithAttachments } else -> sortedMessages } } else { val sortedMessages = messages - .map { it to calculateMatchRatio(it, query) } + .map { it to calculateMatchRatio(it, lastSearchQuery) } .sortedWith(compareBy> { -it.second }.thenByDescending { it.first.date }) .filter { it.second > 6000 } .map { it.first } return when { - onlyUnread && onlyWithAttachments -> sortedMessages.filter { it.unread == onlyUnread && it.hasAttachments == onlyWithAttachments } - onlyUnread -> sortedMessages.filter { it.unread == onlyUnread } + (onlyUnread == true) && onlyWithAttachments -> sortedMessages.filter { it.unread == onlyUnread && it.hasAttachments == onlyWithAttachments } + (onlyUnread == true) -> sortedMessages.filter { it.unread == onlyUnread } onlyWithAttachments -> sortedMessages.filter { it.hasAttachments == onlyWithAttachments } else -> sortedMessages } } } - private fun updateData(data: List) { - view?.run { - showEmpty(data.isEmpty()) - showContent(true) - showErrorView(false) - val newItems = - listOf(MessageTabDataItem.Header) + data.map { MessageTabDataItem.MessageItem(it) } - updateData(newItems, folder.id == MessageFolder.SENT.id) + private fun updateDataInView() { + val data = getFilteredData() + + val list = buildList { + add( + MessageTabDataItem.FilterHeader( + onlyUnread = onlyUnread.takeIf { folder != MessageFolder.SENT }, + onlyWithAttachments = onlyWithAttachments, + isEnabled = !isActionMode + ) + ) + + addAll(data.map { message -> + MessageTabDataItem.MessageItem( + message = message, + isSelected = messagesToDelete.any { it.messageGlobalKey == message.messageGlobalKey }, + isActionMode = isActionMode + ) + }) } + + view?.updateData(list) } private fun calculateMatchRatio(message: Message, query: String): Int { val subjectRatio = FuzzySearch.tokenSortPartialRatio(query.lowercase(), message.subject) - val senderOrRecipientRatio = FuzzySearch.tokenSortPartialRatio( + val correspondentsRatio = FuzzySearch.tokenSortPartialRatio( query.lowercase(), - if (message.sender.isNotEmpty()) message.sender.lowercase() - else message.recipient.lowercase() + message.correspondents ) val dateRatio = listOf( @@ -266,7 +364,7 @@ class MessageTabPresenter @Inject constructor( return (subjectRatio.toDouble().pow(2) - + senderOrRecipientRatio.toDouble().pow(2) + + correspondentsRatio.toDouble().pow(2) + dateRatio.toDouble().pow(2) * 2 ).toInt() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabView.kt index a856da3bc..bfa43b209 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabView.kt @@ -7,15 +7,15 @@ interface MessageTabView : BaseView { val isViewEmpty: Boolean - var onlyUnread: Boolean? - - var onlyWithAttachments: Boolean - fun initView() fun resetListPosition() - fun updateData(data: List, hide: Boolean) + fun updateData(data: List) + + fun updateActionModeTitle(selectedMessagesSize: Int) + + fun updateSelectAllMenu(isAllSelected: Boolean) fun showProgress(show: Boolean) @@ -25,8 +25,12 @@ interface MessageTabView : BaseView { fun showEmpty(show: Boolean) + fun showMessagesDeleted() + fun showErrorView(show: Boolean) + fun notifyParentShowNewMessage(show: Boolean) + fun setErrorDetails(message: String) fun showRefresh(show: Boolean) @@ -34,4 +38,12 @@ interface MessageTabView : BaseView { fun openMessage(message: Message) fun notifyParentDataLoaded() + + fun notifyParentShowActionMode(show: Boolean) + + fun hideKeyboard() + + fun showActionMode(show: Boolean) + + fun showRecyclerBottomPadding(show: Boolean) } 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 53049891a..36a720e53 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 @@ -1,6 +1,6 @@ package io.github.wulkanowy.ui.modules.mobiledevice -import io.github.wulkanowy.data.Status +import io.github.wulkanowy.data.* import io.github.wulkanowy.data.db.entities.MobileDevice import io.github.wulkanowy.data.repositories.MobileDeviceRepository import io.github.wulkanowy.data.repositories.SemesterRepository @@ -8,10 +8,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.afterLoading -import io.github.wulkanowy.utils.flowWithResource -import io.github.wulkanowy.utils.flowWithResourceIn -import kotlinx.coroutines.flow.onEach import timber.log.Timber import javax.inject.Inject @@ -52,49 +48,39 @@ class MobileDevicePresenter @Inject constructor( private fun loadData(forceRefresh: Boolean = false) { Timber.i("Loading mobile devices data started") - flowWithResourceIn { + flatResourceFlow { val student = studentRepository.getCurrentStudent() val semester = semesterRepository.getCurrentSemester(student) mobileDeviceRepository.getDevices(student, semester, forceRefresh) - }.onEach { - when (it.status) { - Status.LOADING -> { - if (!it.data.isNullOrEmpty()) { - view?.run { - enableSwipe(true) - showRefresh(true) - showProgress(false) - showContent(true) - updateData(it.data) - } - } - } - Status.SUCCESS -> { - Timber.i("Loading mobile devices result: Success") - view?.run { - updateData(it.data!!) - showContent(it.data.isNotEmpty()) - showEmpty(it.data.isEmpty()) - showErrorView(false) - } - analytics.logEvent( - "load_data", - "type" to "devices", - "items" to it.data!!.size - ) - } - Status.ERROR -> { - Timber.i("Loading mobile devices result: An exception occurred") - errorHandler.dispatch(it.error!!) + } + .logResourceStatus("load mobile devices data") + .onResourceData { + view?.run { + enableSwipe(true) + showProgress(false) + showErrorView(false) + showContent(it.isNotEmpty()) + showEmpty(it.isEmpty()) + updateData(it) } } - }.afterLoading { - view?.run { - showRefresh(false) - showProgress(false) - enableSwipe(true) + .onResourceIntermediate { view?.showRefresh(true) } + .onResourceSuccess { + analytics.logEvent( + "load_data", + "type" to "devices", + "items" to it.size + ) } - }.launch() + .onResourceNotLoading { + view?.run { + enableSwipe(true) + showProgress(false) + showRefresh(false) + } + } + .onResourceError(errorHandler::dispatch) + .launch() } private fun showErrorViewOnError(message: String, error: Throwable) { @@ -128,25 +114,19 @@ class MobileDevicePresenter @Inject constructor( } fun onUnregisterConfirmed(device: MobileDevice) { - flowWithResource { + resourceFlow { val student = studentRepository.getCurrentStudent() val semester = semesterRepository.getCurrentSemester(student) mobileDeviceRepository.unregisterDevice(student, semester, device) - }.onEach { - when (it.status) { - Status.LOADING -> Timber.i("Unregister device started") - Status.SUCCESS -> { - Timber.i("Unregister device result: Success") - view?.run { - showProgress(false) - enableSwipe(true) - } - } - Status.ERROR -> { - Timber.i("Unregister device result: An exception occurred") - errorHandler.dispatch(it.error!!) + } + .logResourceStatus("unregister device") + .onResourceSuccess { + view?.run { + showProgress(false) + enableSwipe(true) } } - }.launch("unregister") + .onResourceError(errorHandler::dispatch) + .launch("unregister") } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/token/MobileDeviceTokenPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/token/MobileDeviceTokenPresenter.kt index 5e7110ee5..875b73ad7 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/token/MobileDeviceTokenPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/token/MobileDeviceTokenPresenter.kt @@ -1,15 +1,12 @@ package io.github.wulkanowy.ui.modules.mobiledevice.token -import io.github.wulkanowy.data.Status +import io.github.wulkanowy.data.* import io.github.wulkanowy.data.repositories.MobileDeviceRepository 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.AnalyticsHelper -import io.github.wulkanowy.utils.afterLoading -import io.github.wulkanowy.utils.flowWithResource -import kotlinx.coroutines.flow.onEach import timber.log.Timber import javax.inject.Inject @@ -29,29 +26,29 @@ class MobileDeviceTokenPresenter @Inject constructor( } private fun loadData() { - flowWithResource { + resourceFlow { val student = studentRepository.getCurrentStudent() val semester = semesterRepository.getCurrentSemester(student) mobileDeviceRepository.getToken(student, semester) - }.onEach { - when (it.status) { - Status.LOADING -> Timber.i("Mobile device registration data started") - Status.SUCCESS -> { - Timber.i("Mobile device registration result: Success") - view?.run { - updateData(it.data!!) - showContent() - } - analytics.logEvent("device_register", "symbol" to it.data!!.token.substring(0, 3)) - } - Status.ERROR -> { - Timber.i("Mobile device registration result: An exception occurred") - view?.closeDialog() - errorHandler.dispatch(it.error!!) + } + .logResourceStatus("load mobile device registration") + .onResourceData { + view?.run { + updateData(it) + showContent() } } - }.afterLoading { - view?.hideLoading() - }.launch() + .onResourceSuccess { + analytics.logEvent( + "device_register", + "symbol" to it.token.substring(0, 3) + ) + } + .onResourceNotLoading { view?.hideLoading() } + .onResourceError { + view?.closeDialog() + errorHandler.dispatch(it) + } + .launch() } } 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 145b12a35..df55abc9c 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 @@ -89,6 +89,11 @@ class MoreFragment : BaseFragment(R.layout.fragment_more), if (::presenter.isInitialized) presenter.onViewReselected() } + override fun onFragmentChanged() { + (parentFragmentManager.fragments.find { it is MessageFragment } as MessageFragment?) + ?.onFragmentChanged() + } + override fun updateData(data: List>) { with(moreAdapter) { items = data 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 10a391820..440565e11 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 @@ -1,6 +1,6 @@ package io.github.wulkanowy.ui.modules.note -import io.github.wulkanowy.data.Status +import io.github.wulkanowy.data.* import io.github.wulkanowy.data.db.entities.Note import io.github.wulkanowy.data.repositories.NoteRepository import io.github.wulkanowy.data.repositories.SemesterRepository @@ -8,9 +8,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.afterLoading -import io.github.wulkanowy.utils.flowWithResource -import io.github.wulkanowy.utils.flowWithResourceIn import kotlinx.coroutines.flow.onEach import timber.log.Timber import javax.inject.Inject @@ -51,51 +48,40 @@ class NotePresenter @Inject constructor( } private fun loadData(forceRefresh: Boolean = false) { - Timber.i("Loading note data started") - - flowWithResourceIn { + flatResourceFlow { val student = studentRepository.getCurrentStudent() val semester = semesterRepository.getCurrentSemester(student) noteRepository.getNotes(student, semester, forceRefresh) - }.onEach { - when (it.status) { - Status.LOADING -> { - if (!it.data.isNullOrEmpty()) { - view?.run { - enableSwipe(true) - showRefresh(true) - showProgress(false) - showContent(true) - updateData(it.data.sortedByDescending { item -> item.date }) - } - } - } - Status.SUCCESS -> { - Timber.i("Loading note result: Success") - view?.apply { - updateData(it.data!!.sortedByDescending { item -> item.date }) - showEmpty(it.data.isEmpty()) - showErrorView(false) - showContent(it.data.isNotEmpty()) - } - analytics.logEvent( - "load_data", - "type" to "note", - "items" to it.data!!.size - ) - } - Status.ERROR -> { - Timber.i("Loading note result: An exception occurred") - errorHandler.dispatch(it.error!!) + } + .logResourceStatus("load note data") + .mapResourceData { it.sortedByDescending { note -> note.date } } + .onResourceData { + view?.run { + enableSwipe(true) + showProgress(false) + showErrorView(false) + showContent(it.isNotEmpty()) + showEmpty(it.isEmpty()) + updateData(it) } } - }.afterLoading { - view?.run { - showRefresh(false) - showProgress(false) - enableSwipe(true) + .onResourceIntermediate { view?.showRefresh(true) } + .onResourceSuccess { + analytics.logEvent( + "load_data", + "type" to "note", + "items" to it.size + ) } - }.launch() + .onResourceNotLoading { + view?.run { + enableSwipe(true) + showProgress(false) + showRefresh(false) + } + } + .onResourceError(errorHandler::dispatch) + .launch() } private fun showErrorViewOnError(message: String, error: Throwable) { @@ -122,14 +108,14 @@ class NotePresenter @Inject constructor( } private fun updateNote(note: Note) { - flowWithResource { noteRepository.updateNote(note) } + resourceFlow { 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 -> { + when (it) { + is Resource.Loading -> Timber.i("Attempt to update note ${note.id}") + is Resource.Success -> Timber.i("Update note result: Success") + is Resource.Error -> { Timber.i("Update note result: An exception occurred") - errorHandler.dispatch(it.error!!) + errorHandler.dispatch(it.error) } } } 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 index 27b3637ad..92c54f45c 100644 --- 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 @@ -7,14 +7,13 @@ 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 = {} + var onItemClickListener: (Notification) -> Unit = {} override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder( ItemNotificationsCenterBinding.inflate(LayoutInflater.from(parent.context), parent, false) @@ -29,7 +28,7 @@ class NotificationsCenterAdapter @Inject constructor() : notificationsCenterItemDate.text = item.date.toFormattedString("HH:mm, d MMM") notificationsCenterItemIcon.setImageResource(item.type.icon) - root.setOnClickListener { onItemClickListener(item.type) } + root.setOnClickListener { onItemClickListener(item) } } } 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 index f3bbc42de..ca71910a4 100644 --- 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 @@ -3,26 +3,14 @@ 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 @@ -54,9 +42,8 @@ class NotificationsCenterFragment : } override fun initView() { - notificationsCenterAdapter.onItemClickListener = { notificationType -> - notificationType.toDestinationFragment() - ?.let { (requireActivity() as MainActivity).pushView(it) } + notificationsCenterAdapter.onItemClickListener = { notification -> + (requireActivity() as MainActivity).pushView(notification.destination.destinationFragment) } with(binding.notificationsCenterRecycler) { @@ -93,20 +80,4 @@ class NotificationsCenterFragment : 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 index 394c23101..de42e5678 100644 --- 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 @@ -48,7 +48,7 @@ class NotificationsCenterPresenter @Inject constructor( emitAll(notificationRepository.getNotifications(studentId)) } .map { notificationList -> notificationList.sortedByDescending { it.date } } - .catch { Timber.i("Loading notifications result: An exception occurred") } + .catch { Timber.i("Loading notifications result: An exception occurred: `$it`") } .onEach { Timber.i("Loading notifications result: Success") 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 ac8c273ea..262398b8a 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 @@ -1,16 +1,13 @@ package io.github.wulkanowy.ui.modules.schoolandteachers.school -import io.github.wulkanowy.data.Status +import io.github.wulkanowy.data.* import io.github.wulkanowy.data.repositories.SchoolRepository 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.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 @@ -65,46 +62,48 @@ class SchoolPresenter @Inject constructor( } private fun loadData(forceRefresh: Boolean = false) { - flowWithResourceIn { + flatResourceFlow { val student = studentRepository.getCurrentStudent() val semester = semesterRepository.getCurrentSemester(student) schoolRepository.getSchoolInfo(student, semester, forceRefresh) - }.onEach { - when (it.status) { - Status.LOADING -> Timber.i("Loading school info started") - Status.SUCCESS -> if (it.data != null) { - Timber.i("Loading teachers result: Success") + } + .logResourceStatus("load school info") + .onResourceData { + if (it != null) { view?.run { - address = it.data.address.ifBlank { null } - contact = it.data.contact.ifBlank { null } - updateData(it.data) + address = it.address.ifBlank { null } + contact = it.contact.ifBlank { null } + updateData(it) showContent(true) showEmpty(false) showErrorView(false) } - analytics.logEvent("load_item", "type" to "school") } else view?.run { Timber.i("Loading school result: No school info found") showContent(!isViewEmpty) showEmpty(isViewEmpty) showErrorView(false) } - Status.ERROR -> { - Timber.i("Loading school result: An exception occurred") - errorHandler.dispatch(it.error!!) + } + .onResourceSuccess { + if (it != null) { + analytics.logEvent("load_item", "type" to "school") } } - }.afterLoading { - view?.run { - hideRefresh() - showProgress(false) - enableSwipe(true) - notifyParentDataLoaded() + .onResourceNotLoading { + view?.run { + hideRefresh() + showProgress(false) + enableSwipe(true) + notifyParentDataLoaded() + } } - }.catch { - errorHandler.dispatch(it) - view?.notifyParentDataLoaded() - }.launch() + .onResourceError(errorHandler::dispatch) + .catch { + errorHandler.dispatch(it) + view?.notifyParentDataLoaded() + } + .launch() } private fun showErrorViewOnError(message: String, error: Throwable) { 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 bd46ff0b2..e2af05c92 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 @@ -1,16 +1,13 @@ package io.github.wulkanowy.ui.modules.schoolandteachers.teacher -import io.github.wulkanowy.data.Status +import io.github.wulkanowy.data.* import io.github.wulkanowy.data.repositories.SemesterRepository import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.repositories.TeacherRepository 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.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 @@ -53,43 +50,41 @@ class TeacherPresenter @Inject constructor( } private fun loadData(forceRefresh: Boolean = false) { - flowWithResourceIn { + flatResourceFlow { val student = studentRepository.getCurrentStudent() val semester = semesterRepository.getCurrentSemester(student) teacherRepository.getTeachers(student, semester, forceRefresh) - }.onEach { - when (it.status) { - Status.LOADING -> Timber.i("Loading teachers data started") - Status.SUCCESS -> { - Timber.i("Loading teachers result: Success") - view?.run { - updateData(it.data!!.filter { item -> item.name.isNotBlank() }) - showContent(it.data.isNotEmpty()) - showEmpty(it.data.isEmpty()) - showErrorView(false) - } - analytics.logEvent( - "load_data", - "type" to "teachers", - "items" to it.data!!.size - ) - } - Status.ERROR -> { - Timber.i("Loading teachers result: An exception occurred") - errorHandler.dispatch(it.error!!) + } + .logResourceStatus("load teachers data") + .onResourceData { + view?.run { + updateData(it.filter { item -> item.name.isNotBlank() }) + showContent(it.isNotEmpty()) + showEmpty(it.isEmpty()) + showErrorView(false) } } - }.afterLoading { - view?.run { - hideRefresh() - showProgress(false) - enableSwipe(true) - notifyParentDataLoaded() + .onResourceSuccess { + analytics.logEvent( + "load_data", + "type" to "teachers", + "items" to it.size + ) } - }.catch { - errorHandler.dispatch(it) - view?.notifyParentDataLoaded() - }.launch() + .onResourceNotLoading { + view?.run { + hideRefresh() + showProgress(false) + enableSwipe(true) + notifyParentDataLoaded() + } + } + .onResourceError(errorHandler::dispatch) + .catch { + errorHandler.dispatch(it) + view?.notifyParentDataLoaded() + } + .launch() } private fun showErrorViewOnError(message: String, error: Throwable) { 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 62c93198d..f77a88335 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 @@ -1,15 +1,12 @@ package io.github.wulkanowy.ui.modules.schoolannouncement -import io.github.wulkanowy.data.Status +import io.github.wulkanowy.data.* import io.github.wulkanowy.data.db.entities.SchoolAnnouncement import io.github.wulkanowy.data.repositories.SchoolAnnouncementRepository 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.afterLoading -import io.github.wulkanowy.utils.flowWithResourceIn -import kotlinx.coroutines.flow.onEach import timber.log.Timber import javax.inject.Inject @@ -52,50 +49,37 @@ class SchoolAnnouncementPresenter @Inject constructor( } private fun loadData(forceRefresh: Boolean = false) { - Timber.i("Loading School announcement data started") - - flowWithResourceIn { + flatResourceFlow { val student = studentRepository.getCurrentStudent() schoolAnnouncementRepository.getSchoolAnnouncements(student, forceRefresh) - }.onEach { - when (it.status) { - Status.LOADING -> { - if (!it.data.isNullOrEmpty()) { - view?.run { - enableSwipe(true) - showRefresh(true) - showErrorView(false) - showProgress(false) - showContent(true) - updateData(it.data) - } - } - } - Status.SUCCESS -> { - Timber.i("Loading School announcement result: Success") - view?.apply { - updateData(it.data!!) - showEmpty(it.data.isEmpty()) - showErrorView(false) - showContent(it.data.isNotEmpty()) - } - analytics.logEvent( - "load_school_announcement", - "items" to it.data!!.size - ) - } - Status.ERROR -> { - Timber.i("Loading School announcement result: An exception occurred") - errorHandler.dispatch(it.error!!) + } + .logResourceStatus("load school announcement") + .onResourceData { + view?.run { + enableSwipe(true) + showProgress(false) + showErrorView(false) + showContent(it.isNotEmpty()) + showEmpty(it.isEmpty()) + updateData(it) } } - }.afterLoading { - view?.run { - showRefresh(false) - showProgress(false) - enableSwipe(true) + .onResourceSuccess { + analytics.logEvent( + "load_school_announcement", + "items" to it.size + ) } - }.launch() + .onResourceIntermediate { view?.showRefresh(true) } + .onResourceNotLoading { + view?.run { + enableSwipe(true) + showProgress(false) + showRefresh(false) + } + } + .onResourceError(errorHandler::dispatch) + .launch("load_data") } private fun showErrorViewOnError(message: String, error: Throwable) { 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 a2265b044..b4ba5bc4b 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 @@ -4,7 +4,6 @@ import android.content.SharedPreferences import android.os.Bundle import android.view.View import androidx.preference.PreferenceFragmentCompat -import com.yariksoffice.lingver.Lingver import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.ui.base.BaseActivity @@ -24,9 +23,6 @@ class AdvancedFragment : PreferenceFragmentCompat(), @Inject lateinit var appInfo: AppInfo - @Inject - lateinit var lingver: Lingver - override val titleStringId get() = R.string.pref_settings_advanced_title override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -68,11 +64,11 @@ class AdvancedFragment : PreferenceFragmentCompat(), override fun onResume() { super.onResume() - preferenceScreen.sharedPreferences.registerOnSharedPreferenceChangeListener(this) + preferenceScreen.sharedPreferences?.registerOnSharedPreferenceChangeListener(this) } override fun onPause() { super.onPause() - preferenceScreen.sharedPreferences.unregisterOnSharedPreferenceChangeListener(this) + preferenceScreen.sharedPreferences?.unregisterOnSharedPreferenceChangeListener(this) } } 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 f603de781..1f6d5143b 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 @@ -80,11 +80,11 @@ class AppearanceFragment : PreferenceFragmentCompat(), override fun onResume() { super.onResume() - preferenceScreen.sharedPreferences.registerOnSharedPreferenceChangeListener(this) + preferenceScreen.sharedPreferences?.registerOnSharedPreferenceChangeListener(this) } override fun onPause() { super.onPause() - preferenceScreen.sharedPreferences.unregisterOnSharedPreferenceChangeListener(this) + preferenceScreen.sharedPreferences?.unregisterOnSharedPreferenceChangeListener(this) } } 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 84fee7179..364ad2137 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 @@ -83,10 +83,10 @@ class NotificationsFragment : PreferenceFragmentCompat(), } override fun onCreateRecyclerView( - inflater: LayoutInflater?, - parent: ViewGroup?, + inflater: LayoutInflater, + parent: ViewGroup, state: Bundle? - ): RecyclerView? = super.onCreateRecyclerView(inflater, parent, state) + ): RecyclerView = super.onCreateRecyclerView(inflater, parent, state) .also { it.itemAnimator = null it.layoutAnimation = null @@ -214,11 +214,11 @@ class NotificationsFragment : PreferenceFragmentCompat(), override fun onResume() { super.onResume() - preferenceScreen.sharedPreferences.registerOnSharedPreferenceChangeListener(this) + preferenceScreen.sharedPreferences?.registerOnSharedPreferenceChangeListener(this) } override fun onPause() { super.onPause() - preferenceScreen.sharedPreferences.unregisterOnSharedPreferenceChangeListener(this) + preferenceScreen.sharedPreferences?.unregisterOnSharedPreferenceChangeListener(this) } } 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 160b7c37b..8477e3222 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 @@ -91,16 +91,16 @@ class SyncFragment : PreferenceFragmentCompat(), } override fun showErrorDetailsDialog(error: Throwable) { - ErrorDialog.newInstance(error).show(childFragmentManager, error.toString()) + ErrorDialog.newInstance(error).show(childFragmentManager, "error_details") } override fun onResume() { super.onResume() - preferenceScreen.sharedPreferences.registerOnSharedPreferenceChangeListener(this) + preferenceScreen.sharedPreferences?.registerOnSharedPreferenceChangeListener(this) } override fun onPause() { super.onPause() - preferenceScreen.sharedPreferences.unregisterOnSharedPreferenceChangeListener(this) + preferenceScreen.sharedPreferences?.unregisterOnSharedPreferenceChangeListener(this) } } 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 0d404a138..1ecb4a6ef 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 @@ -59,7 +59,10 @@ class SyncPresenter @Inject constructor( WorkInfo.State.FAILED -> { showError( syncFailedString, - Throwable(workInfo.outputData.getString("error")) + Throwable( + message = workInfo.outputData.getString("error_message"), + cause = Throwable(workInfo.outputData.getString("error_stack")) + ) ) analytics.logEvent("sync_now", "status" to "failed") } @@ -76,9 +79,7 @@ class SyncPresenter @Inject constructor( } private fun setSyncDateInView() { - val lastSyncDate = preferencesRepository.lasSyncDate - - if (lastSyncDate.year == 1970) return + val lastSyncDate = preferencesRepository.lasSyncDate ?: return view?.setLastSyncDate(lastSyncDate.toFormattedString("dd.MM.yyyy HH:mm:ss")) } 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 5c1524551..cfb628496 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 @@ -15,6 +15,8 @@ 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.openInternetBrowser +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json import javax.inject.Inject @SuppressLint("CustomSplashScreen") @@ -29,26 +31,26 @@ class SplashActivity : BaseActivity(), SplashView companion object { - private const val EXTRA_START_DESTINATION = "start_destination" + private const val EXTRA_START_DESTINATION = "start_destination_json" 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) + destination?.let { putExtra(EXTRA_START_DESTINATION, Json.encodeToString(it)) } flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - installSplashScreen().setKeepVisibleCondition { true } + installSplashScreen().setKeepOnScreenCondition { true } + shortcutsHelper.initializeShortcuts() val externalLink = intent?.getStringExtra(EXTRA_EXTERNAL_URL) - val startDestination = intent?.getSerializableExtra(EXTRA_START_DESTINATION) as Destination? - ?: shortcutsHelper.getDestination(intent) + val startDestinationJson = intent?.getStringExtra(EXTRA_START_DESTINATION) - presenter.onAttachView(this, externalLink, startDestination) + presenter.onAttachView(this, externalLink, startDestinationJson) } override fun openLoginView() { 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 0b7409020..767c885c1 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 @@ -5,16 +5,21 @@ import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.ui.modules.Destination import kotlinx.coroutines.launch +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json import javax.inject.Inject class SplashPresenter @Inject constructor( errorHandler: ErrorHandler, studentRepository: StudentRepository, + private val json: Json ) : BasePresenter(errorHandler, studentRepository) { - fun onAttachView(view: SplashView, externalUrl: String?, startDestination: Destination?) { + fun onAttachView(view: SplashView, externalUrl: String?, startDestinationJson: String?) { super.onAttachView(view) + val startDestination: Destination? = startDestinationJson?.let { json.decodeFromString(it) } + if (!externalUrl.isNullOrBlank()) { view.openExternalUrlAndFinish(externalUrl) return diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/studentinfo/StudentInfoPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/studentinfo/StudentInfoPresenter.kt index 80798b11e..083b590b2 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/studentinfo/StudentInfoPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/studentinfo/StudentInfoPresenter.kt @@ -1,6 +1,6 @@ package io.github.wulkanowy.ui.modules.studentinfo -import io.github.wulkanowy.data.Status +import io.github.wulkanowy.data.* import io.github.wulkanowy.data.db.entities.StudentInfo import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.data.repositories.StudentInfoRepository @@ -8,10 +8,7 @@ import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.utils.AnalyticsHelper -import io.github.wulkanowy.utils.afterLoading -import io.github.wulkanowy.utils.flowWithResourceIn import io.github.wulkanowy.utils.getCurrentOrLast -import kotlinx.coroutines.flow.onEach import timber.log.Timber import javax.inject.Inject @@ -72,51 +69,50 @@ class StudentInfoPresenter @Inject constructor( } private fun loadData(forceRefresh: Boolean = false) { - flowWithResourceIn { + flatResourceFlow { val semester = studentWithSemesters.semesters.getCurrentOrLast() studentInfoRepository.getStudentInfo( student = studentWithSemesters.student, semester = semester, forceRefresh = forceRefresh ) - }.onEach { - when (it.status) { - Status.LOADING -> Timber.i("Loading student info $infoType started") - Status.SUCCESS -> { - val isFamily = infoType == StudentInfoView.Type.FAMILY - val isFirstGuardianEmpty = it.data?.firstGuardian == null - val isSecondGuardianEmpty = it.data?.secondGuardian == null - - if (it.data != null && !(isFamily && isFirstGuardianEmpty && isSecondGuardianEmpty)) { - Timber.i("Loading student info $infoType result: Success") - showCorrectData(it.data) - view?.run { - showContent(true) - showEmpty(false) - showErrorView(false) - } - analytics.logEvent("load_item", "type" to "student_info") - } else { - Timber.i("Loading student info $infoType result: No student or family info found") - view?.run { - showContent(!isViewEmpty) - showEmpty(isViewEmpty) - showErrorView(false) - } + } + .logResourceStatus("load student info $infoType") + .onResourceData { + val isFamily = infoType == StudentInfoView.Type.FAMILY + val isFirstGuardianEmpty = it?.firstGuardian == null + val isSecondGuardianEmpty = it?.secondGuardian == null + if (it != null && !(isFamily && isFirstGuardianEmpty && isSecondGuardianEmpty)) { + Timber.i("Loading student info $infoType result: Success") + showCorrectData(it) + view?.run { + showContent(true) + showEmpty(false) + showErrorView(false) + } + } else { + Timber.i("Loading student info $infoType result: No student or family info found") + view?.run { + showContent(!isViewEmpty) + showEmpty(isViewEmpty) + showErrorView(false) } } - Status.ERROR -> { - Timber.i("Loading student info $infoType result: An exception occurred") - errorHandler.dispatch(it.error!!) + } + .onResourceSuccess { + if (it != null) { + analytics.logEvent("load_item", "type" to "student_info") } } - }.afterLoading { - view?.run { - hideRefresh() - showProgress(false) - enableSwipe(true) + .onResourceNotLoading { + view?.run { + hideRefresh() + showProgress(false) + enableSwipe(true) + } } - }.launch() + .onResourceError(errorHandler::dispatch) + .launch() } private fun showCorrectData(studentInfo: StudentInfo) { 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 422cc50e8..d6917672a 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,121 +1,69 @@ 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.isVisible import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Timetable -import io.github.wulkanowy.data.enums.TimetableMode import io.github.wulkanowy.databinding.ItemTimetableBinding import io.github.wulkanowy.databinding.ItemTimetableSmallBinding import io.github.wulkanowy.utils.getThemeAttrColor -import io.github.wulkanowy.utils.isJustFinished -import io.github.wulkanowy.utils.isShowTimeUntil -import io.github.wulkanowy.utils.left import io.github.wulkanowy.utils.toFormattedString -import io.github.wulkanowy.utils.until -import timber.log.Timber -import java.time.LocalDateTime -import java.util.Timer import javax.inject.Inject -import kotlin.concurrent.timer -class TimetableAdapter @Inject constructor() : RecyclerView.Adapter() { +class TimetableAdapter @Inject constructor() : + ListAdapter(differ) { - private enum class ViewType { - ITEM_NORMAL, - ITEM_SMALL - } - - var onClickListener: (Timetable) -> Unit = {} - - private var showWholeClassPlan = TimetableMode.ONLY_CURRENT_GROUP - - private var showGroupsInPlan: Boolean = false - - private var showTimers: Boolean = false - - private val timers = mutableMapOf() - - private val items = mutableListOf() - - fun submitList( - newTimetable: List, - showWholeClassPlan: TimetableMode = this.showWholeClassPlan, - showGroupsInPlan: Boolean = this.showGroupsInPlan, - showTimers: Boolean = this.showTimers - ) { - val isFlagsDifferent = this.showWholeClassPlan != showWholeClassPlan - || this.showGroupsInPlan != showGroupsInPlan - || this.showTimers != showTimers - - val diffResult = DiffUtil.calculateDiff( - TimetableAdapterDiffCallback( - oldList = items.toMutableList(), - newList = newTimetable, - isFlagsDifferent = isFlagsDifferent - ) - ) - - this.showGroupsInPlan = showGroupsInPlan - this.showTimers = showTimers - this.showWholeClassPlan = showWholeClassPlan - - items.clear() - items.addAll(newTimetable) - - diffResult.dispatchUpdatesTo(this) - } - - fun clearTimers() { - Timber.d("Timetable timers (${timers.size}) cleared") - with(timers) { - forEach { (_, timer) -> - timer?.cancel() - timer?.purge() - } - clear() - } - } - - override fun getItemCount() = items.size - - override fun getItemViewType(position: Int) = when { - !items[position].isStudentPlan && showWholeClassPlan == TimetableMode.SMALL_OTHER_GROUP -> ViewType.ITEM_SMALL.ordinal - else -> ViewType.ITEM_NORMAL.ordinal - } + override fun getItemViewType(position: Int): Int = getItem(position).type.ordinal override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { val inflater = LayoutInflater.from(parent.context) - return when (viewType) { - ViewType.ITEM_NORMAL.ordinal -> ItemViewHolder( - ItemTimetableBinding.inflate(inflater, parent, false) - ) - ViewType.ITEM_SMALL.ordinal -> SmallItemViewHolder( + return when (TimetableItemType.values()[viewType]) { + TimetableItemType.SMALL -> SmallViewHolder( ItemTimetableSmallBinding.inflate(inflater, parent, false) ) - else -> throw IllegalStateException() + TimetableItemType.NORMAL -> NormalViewHolder( + ItemTimetableBinding.inflate(inflater, parent, false) + ) } } + override fun onBindViewHolder( + holder: RecyclerView.ViewHolder, + position: Int, + payloads: MutableList + ) { + if (payloads.isEmpty()) return super.onBindViewHolder(holder, position, payloads) + + if (holder is NormalViewHolder) updateTimeLeft( + binding = holder.binding, + timeLeft = (getItem(position) as TimetableItem.Normal).timeLeft, + ) + } + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - val lesson = items[position] - when (holder) { - is ItemViewHolder -> bindNormalView(holder.binding, lesson, position) - is SmallItemViewHolder -> bindSmallView(holder.binding, lesson) + is SmallViewHolder -> bindSmallView( + binding = holder.binding, + item = getItem(position) as TimetableItem.Small, + ) + is NormalViewHolder -> bindNormalView( + binding = holder.binding, + item = getItem(position) as TimetableItem.Normal, + ) } } - private fun bindSmallView(binding: ItemTimetableSmallBinding, lesson: Timetable) { + private fun bindSmallView(binding: ItemTimetableSmallBinding, item: TimetableItem.Small) { + val lesson = item.lesson + with(binding) { timetableSmallItemNumber.text = lesson.number.toString() timetableSmallItemSubject.text = lesson.subject @@ -127,11 +75,13 @@ class TimetableAdapter @Inject constructor() : RecyclerView.Adapter i < position && !item.isStudentPlan }.size) - ?.let { - if (!it.canceled && it.isStudentPlan) it.end - else null - } - } - - private fun updateTimeLeft(binding: ItemTimetableBinding, lesson: Timetable, position: Int) { - val isShowTimeUntil = lesson.isShowTimeUntil(getPreviousLesson(position)) - val until = lesson.until.plusMinutes(1) - val left = lesson.left?.plusMinutes(1) - val isJustFinished = lesson.isJustFinished - + private fun updateTimeLeft(binding: ItemTimetableBinding, timeLeft: TimeLeft?) { with(binding) { when { // before lesson - isShowTimeUntil -> { - Timber.d("Show time until lesson: $position") + timeLeft?.until != null -> { timetableItemTimeLeft.visibility = GONE with(timetableItemTimeUntil) { visibility = VISIBLE @@ -194,14 +112,13 @@ class TimetableAdapter @Inject constructor() : RecyclerView.Adapter { - Timber.d("Show time left lesson: $position") + timeLeft?.left != null -> { timetableItemTimeUntil.visibility = GONE with(timetableItemTimeLeft) { visibility = VISIBLE @@ -209,14 +126,13 @@ class TimetableAdapter @Inject constructor() : RecyclerView.Adapter { - Timber.d("Show just finished lesson: $position") + timeLeft?.isJustFinished == true -> { timetableItemTimeUntil.visibility = GONE timetableItemTimeLeft.visibility = VISIBLE timetableItemTimeLeft.text = root.context.getString(R.string.timetable_finished) @@ -230,9 +146,7 @@ class TimetableAdapter @Inject constructor() : RecyclerView.Adapter, - private val newList: List, - private val isFlagsDifferent: Boolean - ) : DiffUtil.Callback() { + companion object { + private val differ = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: TimetableItem, newItem: TimetableItem): Boolean = + when { + oldItem is TimetableItem.Small && newItem is TimetableItem.Small -> { + oldItem.lesson.start == newItem.lesson.start + } + oldItem is TimetableItem.Normal && newItem is TimetableItem.Normal -> { + oldItem.lesson.start == newItem.lesson.start + } + else -> oldItem == newItem + } - override fun getOldListSize() = oldList.size + override fun areContentsTheSame(oldItem: TimetableItem, newItem: TimetableItem) = + oldItem == newItem - override fun getNewListSize() = newList.size - - override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) = - oldList[oldItemPosition].id == newList[newItemPosition].id - - override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) = - oldList[oldItemPosition] == newList[newItemPosition] && !isFlagsDifferent + override fun getChangePayload(oldItem: TimetableItem, newItem: TimetableItem): Any? { + return if (oldItem is TimetableItem.Normal && newItem is TimetableItem.Normal) { + if (oldItem.lesson == newItem.lesson && oldItem.timeLeft != newItem.timeLeft) { + "time_left" + } else super.getChangePayload(oldItem, newItem) + } else super.getChangePayload(oldItem, newItem) + } + } } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableDialog.kt index 5bbaaa60f..c9243b12e 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableDialog.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableDialog.kt @@ -16,7 +16,7 @@ import io.github.wulkanowy.utils.capitalise import io.github.wulkanowy.utils.getThemeAttrColor import io.github.wulkanowy.utils.lifecycleAwareVariable import io.github.wulkanowy.utils.toFormattedString -import java.time.LocalDateTime +import java.time.Instant class TimetableDialog : DialogFragment() { @@ -192,7 +192,7 @@ class TimetableDialog : DialogFragment() { } @SuppressLint("SetTextI18n") - private fun setTime(start: LocalDateTime, end: LocalDateTime) { + private fun setTime(start: Instant, end: Instant) { binding.timetableDialogTimeValue.text = "${start.toFormattedString("HH:mm")} - ${end.toFormattedString("HH:mm")}" } 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 b49d11c12..fdd4aface 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 @@ -9,12 +9,9 @@ import android.view.View.GONE import android.view.View.VISIBLE import androidx.core.text.parseAsHtml import androidx.recyclerview.widget.LinearLayoutManager -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.data.db.entities.Timetable -import io.github.wulkanowy.data.enums.TimetableMode import io.github.wulkanowy.databinding.FragmentTimetableBinding import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.modules.main.MainActivity @@ -22,13 +19,7 @@ import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.modules.timetable.additional.AdditionalLessonsFragment import io.github.wulkanowy.ui.modules.timetable.completed.CompletedLessonsFragment import io.github.wulkanowy.ui.widgets.DividerItemDecoration -import io.github.wulkanowy.utils.SchoolDaysValidator -import io.github.wulkanowy.utils.dpToPx -import io.github.wulkanowy.utils.getThemeAttrColor -import io.github.wulkanowy.utils.schoolYearEnd -import io.github.wulkanowy.utils.schoolYearStart -import io.github.wulkanowy.utils.toLocalDateTime -import io.github.wulkanowy.utils.toTimestamp +import io.github.wulkanowy.utils.* import java.time.LocalDate import javax.inject.Inject @@ -77,8 +68,6 @@ class TimetableFragment : BaseFragment(R.layout.fragme } override fun initView() { - timetableAdapter.onClickListener = presenter::onTimetableItemSelected - with(binding.timetableRecycler) { layoutManager = LinearLayoutManager(context) adapter = timetableAdapter @@ -114,18 +103,8 @@ class TimetableFragment : BaseFragment(R.layout.fragme } } - override fun updateData( - data: List, - showWholeClassPlanType: TimetableMode, - showGroupsInPlanType: Boolean, - showTimetableTimers: Boolean - ) { - timetableAdapter.submitList( - newTimetable = data.toMutableList(), - showGroupsInPlan = showGroupsInPlanType, - showTimers = showTimetableTimers, - showWholeClassPlan = showWholeClassPlanType - ) + override fun updateData(data: List) { + timetableAdapter.submitList(data) } override fun clearData() { @@ -193,29 +172,15 @@ class TimetableFragment : BaseFragment(R.layout.fragme (activity as? MainActivity)?.showDialogFragment(TimetableDialog.newInstance(lesson)) } - override fun showDatePickerDialog(currentDate: LocalDate) { - val baseDate = currentDate.schoolYearStart - val rangeStart = baseDate.toTimestamp() - val rangeEnd = LocalDate.now().schoolYearEnd.toTimestamp() - - val constraintsBuilder = CalendarConstraints.Builder().apply { - setValidator(SchoolDaysValidator(rangeStart, rangeEnd)) - setStart(rangeStart) - setEnd(rangeEnd) - } - val datePicker = MaterialDatePicker.Builder.datePicker() - .setCalendarConstraints(constraintsBuilder.build()) - .setSelection(currentDate.toTimestamp()) - .build() - - datePicker.addOnPositiveButtonClickListener { - val date = it.toLocalDateTime() - presenter.onDateSet(date.year, date.monthValue, date.dayOfMonth) - } - - if (!parentFragmentManager.isStateSaved) { - datePicker.show(parentFragmentManager, null) - } + override fun showDatePickerDialog(selectedDate: LocalDate) { + openMaterialDatePicker( + selected = selectedDate, + rangeStart = selectedDate.firstSchoolDayInSchoolYear, + rangeEnd = LocalDate.now().lastSchoolDayInSchoolYear, + onDateSelected = { + presenter.onDateSet(it.year, it.monthValue, it.dayOfMonth) + } + ) } override fun openAdditionalLessonsView() { @@ -232,7 +197,6 @@ class TimetableFragment : BaseFragment(R.layout.fragme } override fun onDestroyView() { - timetableAdapter.clearTimers() presenter.onDetachView() super.onDestroyView() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableItem.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableItem.kt new file mode 100644 index 000000000..92716ace8 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableItem.kt @@ -0,0 +1,30 @@ +package io.github.wulkanowy.ui.modules.timetable + +import io.github.wulkanowy.data.db.entities.Timetable +import java.time.Duration + +sealed class TimetableItem(val type: TimetableItemType) { + + data class Small( + val lesson: Timetable, + val onClick: (Timetable) -> Unit, + ) : TimetableItem(TimetableItemType.SMALL) + + data class Normal( + val lesson: Timetable, + val showGroupsInPlan: Boolean, + val timeLeft: TimeLeft?, + val onClick: (Timetable) -> Unit, + ) : TimetableItem(TimetableItemType.NORMAL) +} + +data class TimeLeft( + val until: Duration?, + val left: Duration?, + val isJustFinished: Boolean, +) + +enum class TimetableItemType { + SMALL, + NORMAL, +} 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 87f5cac3d..d06874082 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 @@ -1,7 +1,6 @@ package io.github.wulkanowy.ui.modules.timetable -import android.annotation.SuppressLint -import io.github.wulkanowy.data.Status +import io.github.wulkanowy.data.* import io.github.wulkanowy.data.db.entities.Timetable import io.github.wulkanowy.data.enums.TimetableMode import io.github.wulkanowy.data.repositories.PreferencesRepository @@ -10,25 +9,17 @@ import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.repositories.TimetableRepository 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.afterLoading -import io.github.wulkanowy.utils.capitalise -import io.github.wulkanowy.utils.flowWithResourceIn -import io.github.wulkanowy.utils.getLastSchoolDayIfHoliday -import io.github.wulkanowy.utils.isHolidays -import io.github.wulkanowy.utils.nextOrSameSchoolDay -import io.github.wulkanowy.utils.nextSchoolDay -import io.github.wulkanowy.utils.previousSchoolDay -import io.github.wulkanowy.utils.toFormattedString +import io.github.wulkanowy.utils.* import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.onEach import timber.log.Timber +import java.time.Instant import java.time.LocalDate -import java.time.LocalDate.now -import java.time.LocalDate.of -import java.time.LocalDate.ofEpochDay +import java.time.LocalDate.* +import java.util.* import javax.inject.Inject +import kotlin.concurrent.timer class TimetablePresenter @Inject constructor( errorHandler: ErrorHandler, @@ -46,6 +37,8 @@ class TimetablePresenter @Inject constructor( private lateinit var lastError: Throwable + private var tickTimer: Timer? = null + fun onAttachView(view: TimetableView, date: Long?) { super.onAttachView(view) view.initView() @@ -94,21 +87,20 @@ class TimetablePresenter @Inject constructor( fun onViewReselected() { Timber.i("Timetable view is reselected") - view?.also { view -> + view?.let { view -> if (view.currentStackSize == 1) { - baseDate.also { - if (currentDate != it) { - reloadView(it) - loadData() - } else if (!view.isViewEmpty) view.resetView() - } - } else view.popView() - } - } + baseDate = now().nextOrSameSchoolDay - fun onTimetableItemSelected(lesson: Timetable) { - Timber.i("Select timetable item ${lesson.id}") - view?.showTimetableDialog(lesson) + if (currentDate != baseDate) { + reloadView(baseDate) + loadData() + } else if (!view.isViewEmpty) { + view.resetView() + } + } else { + view.popView() + } + } } fun onAdditionalLessonsSwitchSelected(): Boolean { @@ -135,71 +127,106 @@ class TimetablePresenter @Inject constructor( } private fun loadData(forceRefresh: Boolean = false) { - Timber.i("Loading timetable data started") - - flowWithResourceIn { + flatResourceFlow { val student = studentRepository.getCurrentStudent() val semester = semesterRepository.getCurrentSemester(student) timetableRepository.getTimetable( - student, semester, currentDate, currentDate, forceRefresh + student = student, + semester = semester, + start = currentDate, + end = currentDate, + forceRefresh = forceRefresh, + timetableType = TimetableRepository.TimetableType.NORMAL ) - }.onEach { - when (it.status) { - Status.LOADING -> { - if (!it.data?.lessons.isNullOrEmpty()) { - view?.run { - enableSwipe(true) - showRefresh(true) - showErrorView(false) - showProgress(false) - showContent(true) - updateData(it.data!!.lessons) - } - } - } - Status.SUCCESS -> { - Timber.i("Loading timetable result: Success") - view?.apply { - updateData(it.data!!.lessons) - showEmpty(it.data.lessons.isEmpty()) - setDayHeaderMessage(it.data.headers.singleOrNull { header -> - header.date == currentDate - }?.content) - showErrorView(false) - showContent(it.data.lessons.isNotEmpty()) - } - analytics.logEvent( - "load_data", - "type" to "timetable", - "items" to it.data!!.lessons.size - ) - } - Status.ERROR -> { - Timber.i("Loading timetable result: An exception occurred") - errorHandler.dispatch(it.error!!) + } + .logResourceStatus("load timetable data") + .onResourceData { + view?.run { + enableSwipe(true) + showProgress(false) + showErrorView(false) + showContent(it.lessons.isNotEmpty()) + showEmpty(it.lessons.isEmpty()) + updateData(it.lessons) + setDayHeaderMessage(it.headers.singleOrNull { header -> header.date == currentDate }?.content) } } - }.afterLoading { - view?.run { - showRefresh(false) - showProgress(false) - enableSwipe(true) + .onResourceIntermediate { view?.showRefresh(true) } + .onResourceSuccess { + analytics.logEvent( + "load_data", + "type" to "timetable", + "items" to it.lessons.size + ) } - }.launch() + .onResourceNotLoading { + view?.run { + enableSwipe(true) + showProgress(false) + showRefresh(false) + } + } + .onResourceError(errorHandler::dispatch) + .launch() } private fun updateData(lessons: List) { - view?.updateData( - showWholeClassPlanType = prefRepository.showWholeClassPlan, - showGroupsInPlanType = prefRepository.showGroupsInPlan, - showTimetableTimers = prefRepository.showTimetableTimers, - data = createItems(lessons) + tickTimer?.cancel() + + if (!prefRepository.showTimetableTimers) { + view?.updateData(createItems(lessons)) + } else { + tickTimer = timer(period = 2_000) { + view?.updateData(createItems(lessons)) + } + } + } + + private fun createItems(items: List): List { + val filteredItems = items + .filter { + if (prefRepository.showWholeClassPlan == TimetableMode.ONLY_CURRENT_GROUP) { + it.isStudentPlan + } else true + }.sortedWith( + compareBy({ item -> item.number }, { item -> !item.isStudentPlan }) + ) + + return filteredItems.mapIndexed { i, it -> + if (it.isStudentPlan) TimetableItem.Normal( + lesson = it, + showGroupsInPlan = prefRepository.showGroupsInPlan, + timeLeft = filteredItems.getTimeLeftForLesson(it, i), + onClick = ::onTimetableItemSelected + ) else TimetableItem.Small( + lesson = it, + onClick = ::onTimetableItemSelected + ) + } + } + + private fun List.getTimeLeftForLesson(lesson: Timetable, index: Int): TimeLeft { + val isShowTimeUntil = lesson.isShowTimeUntil(getPreviousLesson(index)) + return TimeLeft( + until = lesson.until.plusMinutes(1).takeIf { isShowTimeUntil }, + left = lesson.left?.plusMinutes(1), + isJustFinished = lesson.isJustFinished, ) } - private fun createItems(items: List) = items.filter { item -> - if (prefRepository.showWholeClassPlan == TimetableMode.ONLY_CURRENT_GROUP) item.isStudentPlan else true - }.sortedWith(compareBy({ item -> item.number }, { item -> !item.isStudentPlan })) + private fun List.getPreviousLesson(position: Int): Instant? { + return filter { it.isStudentPlan } + .getOrNull(position - 1 - filterIndexed { i, item -> i < position && !item.isStudentPlan }.size) + ?.let { + if (!it.canceled && it.isStudentPlan) it.end + else null + } + } + + private fun onTimetableItemSelected(lesson: Timetable) { + Timber.i("Select timetable item ${lesson.id}") + view?.showTimetableDialog(lesson) + } private fun showErrorViewOnError(message: String, error: Throwable) { view?.run { @@ -227,7 +254,6 @@ class TimetablePresenter @Inject constructor( } } - @SuppressLint("DefaultLocale") private fun reloadNavigation() { view?.apply { showPreButton(!currentDate.minusDays(1).isHolidays) @@ -235,4 +261,10 @@ class TimetablePresenter @Inject constructor( updateNavigationDay(currentDate.toFormattedString("EEEE, dd.MM").capitalise()) } } + + override fun onDetachView() { + tickTimer?.cancel() + tickTimer = null + super.onDetachView() + } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableView.kt index eaf75cb0e..8cfb26204 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableView.kt @@ -1,7 +1,6 @@ package io.github.wulkanowy.ui.modules.timetable import io.github.wulkanowy.data.db.entities.Timetable -import io.github.wulkanowy.data.enums.TimetableMode import io.github.wulkanowy.ui.base.BaseView import java.time.LocalDate @@ -13,12 +12,7 @@ interface TimetableView : BaseView { fun initView() - fun updateData( - data: List, - showWholeClassPlanType: TimetableMode, - showGroupsInPlanType: Boolean, - showTimetableTimers: Boolean - ) + fun updateData(data: List) fun updateNavigationDay(date: String) @@ -48,7 +42,7 @@ interface TimetableView : BaseView { fun showTimetableDialog(lesson: Timetable) - fun showDatePickerDialog(currentDate: LocalDate) + fun showDatePickerDialog(selectedDate: LocalDate) fun popView() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/AdditionalLessonsAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/AdditionalLessonsAdapter.kt index fdc8b8874..c2ce80289 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/AdditionalLessonsAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/AdditionalLessonsAdapter.kt @@ -3,6 +3,7 @@ package io.github.wulkanowy.ui.modules.timetable.additional import android.annotation.SuppressLint import android.view.LayoutInflater import android.view.ViewGroup +import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView import io.github.wulkanowy.data.db.entities.TimetableAdditional import io.github.wulkanowy.databinding.ItemTimetableAdditionalBinding @@ -14,6 +15,8 @@ class AdditionalLessonsAdapter @Inject constructor() : var items = emptyList() + var onDeleteClickListener: (timetableAdditional: TimetableAdditional) -> Unit = {} + override fun getItemCount() = items.size override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ItemViewHolder( @@ -25,8 +28,12 @@ class AdditionalLessonsAdapter @Inject constructor() : val item = items[position] with(holder.binding) { - additionalLessonItemTime.text = "${item.start.toFormattedString("HH:mm")} - ${item.end.toFormattedString("HH:mm")}" + additionalLessonItemTime.text = + "${item.start.toFormattedString("HH:mm")} - ${item.end.toFormattedString("HH:mm")}" additionalLessonItemSubject.text = item.subject + + additionalLessonItemDelete.isVisible = item.isAddedByUser + additionalLessonItemDelete.setOnClickListener { onDeleteClickListener(item) } } } 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 e8fb9e440..043fa1f7d 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 @@ -2,23 +2,22 @@ package io.github.wulkanowy.ui.modules.timetable.additional import android.os.Bundle import android.view.View +import androidx.appcompat.app.AlertDialog import androidx.recyclerview.widget.LinearLayoutManager -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.data.db.entities.TimetableAdditional import io.github.wulkanowy.databinding.FragmentTimetableAdditionalBinding 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.timetable.additional.add.AdditionalLessonAddDialog import io.github.wulkanowy.ui.widgets.DividerItemDecoration -import io.github.wulkanowy.utils.SchoolDaysValidator import io.github.wulkanowy.utils.dpToPx +import io.github.wulkanowy.utils.firstSchoolDayInSchoolYear import io.github.wulkanowy.utils.getThemeAttrColor -import io.github.wulkanowy.utils.schoolYearEnd -import io.github.wulkanowy.utils.schoolYearStart -import io.github.wulkanowy.utils.toLocalDateTime -import io.github.wulkanowy.utils.toTimestamp +import io.github.wulkanowy.utils.lastSchoolDayInSchoolYear +import io.github.wulkanowy.utils.openMaterialDatePicker import java.time.LocalDate import javax.inject.Inject @@ -53,7 +52,9 @@ class AdditionalLessonsFragment : override fun initView() { with(binding.additionalLessonsRecycler) { layoutManager = LinearLayoutManager(context) - adapter = additionalLessonsAdapter + adapter = additionalLessonsAdapter.apply { + onDeleteClickListener = { presenter.onDeleteLessonsSelected(it) } + } addItemDecoration(DividerItemDecoration(context)) } @@ -61,9 +62,7 @@ class AdditionalLessonsFragment : additionalLessonsSwipe.setOnRefreshListener(presenter::onSwipeRefresh) additionalLessonsSwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary)) additionalLessonsSwipe.setProgressBackgroundColorSchemeColor( - requireContext().getThemeAttrColor( - R.attr.colorSwipeRefresh - ) + requireContext().getThemeAttrColor(R.attr.colorSwipeRefresh) ) additionalLessonsErrorRetry.setOnClickListener { presenter.onRetry() } additionalLessonsErrorDetails.setOnClickListener { presenter.onDetailsClick() } @@ -72,6 +71,8 @@ class AdditionalLessonsFragment : additionalLessonsNavDate.setOnClickListener { presenter.onPickDate() } additionalLessonsNextButton.setOnClickListener { presenter.onNextDay() } + openAddAdditionalLessonButton.setOnClickListener { presenter.onAdditionalLessonAddButtonClicked() } + additionalLessonsNavContainer.elevation = requireContext().dpToPx(8f) } } @@ -90,6 +91,10 @@ class AdditionalLessonsFragment : } } + override fun showSuccessMessage() { + getString(R.string.additional_lessons_delete_success) + } + override fun updateNavigationDay(date: String) { binding.additionalLessonsNavDate.text = date } @@ -131,30 +136,33 @@ class AdditionalLessonsFragment : binding.additionalLessonsNextButton.visibility = if (show) View.VISIBLE else View.INVISIBLE } - override fun showDatePickerDialog(currentDate: LocalDate) { + override fun showAddAdditionalLessonDialog() { + (activity as? MainActivity)?.showDialogFragment(AdditionalLessonAddDialog.newInstance()) + } + + override fun showDatePickerDialog(selectedDate: LocalDate) { val now = LocalDate.now() - val startOfSchoolYear = now.schoolYearStart.toTimestamp() - val endOfSchoolYear = now.schoolYearEnd.toTimestamp() - val constraintsBuilder = CalendarConstraints.Builder().apply { - setValidator(SchoolDaysValidator(startOfSchoolYear, endOfSchoolYear)) - setStart(startOfSchoolYear) - setEnd(endOfSchoolYear) - } - val datePicker = - MaterialDatePicker.Builder.datePicker() - .setCalendarConstraints(constraintsBuilder.build()) - .setSelection(currentDate.toTimestamp()) - .build() + openMaterialDatePicker( + selected = selectedDate, + rangeStart = now.firstSchoolDayInSchoolYear, + rangeEnd = now.lastSchoolDayInSchoolYear, + onDateSelected = { + presenter.onDateSet(it.year, it.monthValue, it.dayOfMonth) + } + ) + } - datePicker.addOnPositiveButtonClickListener { - val date = it.toLocalDateTime() - presenter.onDateSet(date.year, date.monthValue, date.dayOfMonth) - } - - if (!parentFragmentManager.isStateSaved) { - datePicker.show(parentFragmentManager, null) - } + override fun showDeleteLessonDialog(timetableAdditional: TimetableAdditional) { + AlertDialog.Builder(requireContext()) + .setTitle(getString(R.string.additional_lessons_delete_title)) + .setItems( + arrayOf( + getString(R.string.additional_lessons_delete_one), + getString(R.string.additional_lessons_delete_series) + ) + ) { _, position -> presenter.onDeleteDialogSelectItem(position, timetableAdditional) } + .show() } override fun onSaveInstanceState(outState: Bundle) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/AdditionalLessonsPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/AdditionalLessonsPresenter.kt index 3496e141a..d0a01b38c 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/AdditionalLessonsPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/AdditionalLessonsPresenter.kt @@ -1,25 +1,18 @@ package io.github.wulkanowy.ui.modules.timetable.additional import android.annotation.SuppressLint -import io.github.wulkanowy.data.Status +import io.github.wulkanowy.data.* +import io.github.wulkanowy.data.db.entities.TimetableAdditional import io.github.wulkanowy.data.repositories.SemesterRepository import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.repositories.TimetableRepository import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.ErrorHandler -import io.github.wulkanowy.utils.AnalyticsHelper -import io.github.wulkanowy.utils.afterLoading -import io.github.wulkanowy.utils.capitalise -import io.github.wulkanowy.utils.flowWithResourceIn -import io.github.wulkanowy.utils.getLastSchoolDayIfHoliday -import io.github.wulkanowy.utils.isHolidays -import io.github.wulkanowy.utils.nextOrSameSchoolDay -import io.github.wulkanowy.utils.nextSchoolDay -import io.github.wulkanowy.utils.previousSchoolDay -import io.github.wulkanowy.utils.toFormattedString +import io.github.wulkanowy.utils.* import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch import timber.log.Timber import java.time.LocalDate import javax.inject.Inject @@ -63,6 +56,10 @@ class AdditionalLessonsPresenter @Inject constructor( view?.showDatePickerDialog(currentDate) } + fun onAdditionalLessonAddButtonClicked() { + view?.showAddAdditionalLessonDialog() + } + fun onDateSet(year: Int, month: Int, day: Int) { loadData(LocalDate.of(year, month, day)) reloadView() @@ -98,42 +95,77 @@ class AdditionalLessonsPresenter @Inject constructor( }.launch("holidays") } + fun onDeleteLessonsSelected(timetableAdditional: TimetableAdditional) { + if (timetableAdditional.repeatId == null) { + deleteAdditionalLessons(timetableAdditional, false) + } else { + view?.showDeleteLessonDialog(timetableAdditional) + } + } + + fun onDeleteDialogSelectItem(position: Int, timetableAdditional: TimetableAdditional) { + deleteAdditionalLessons(timetableAdditional, position == 1) + } + + private fun deleteAdditionalLessons( + timetableAdditional: TimetableAdditional, + deleteSeries: Boolean + ) { + presenterScope.launch { + Timber.i("Additional Lesson delete start") + runCatching { timetableRepository.deleteAdditional(timetableAdditional, deleteSeries) } + .onSuccess { + Timber.i("Additional Lesson delete: Success") + view?.showSuccessMessage() + } + .onFailure { + Timber.i("Additional Lesson delete result: An exception occurred") + errorHandler.dispatch(it) + } + } + } + private fun loadData(date: LocalDate, forceRefresh: Boolean = false) { currentDate = date - flowWithResourceIn { + flatResourceFlow { val student = studentRepository.getCurrentStudent() val semester = semesterRepository.getCurrentSemester(student) - timetableRepository.getTimetable(student, semester, date, date, forceRefresh, true) - }.onEach { - when (it.status) { - Status.LOADING -> Timber.i("Loading additional lessons data started") - Status.SUCCESS -> { - Timber.i("Loading additional lessons lessons result: Success") - view?.apply { - updateData(it.data!!.additional.sortedBy { item -> item.date }) - showEmpty(it.data.additional.isEmpty()) - showErrorView(false) - showContent(it.data.additional.isNotEmpty()) - } - analytics.logEvent( - "load_data", - "type" to "additional_lessons", - "items" to it.data!!.additional.size - ) - } - Status.ERROR -> { - Timber.i("Loading additional lessons result: An exception occurred") - errorHandler.dispatch(it.error!!) + timetableRepository.getTimetable( + student = student, + semester = semester, + start = date, + end = date, + forceRefresh = forceRefresh, + refreshAdditional = true, + timetableType = TimetableRepository.TimetableType.ADDITIONAL + ) + } + .logResourceStatus("load additional lessons") + .onResourceData { + view?.apply { + updateData(it.additional.sortedBy { item -> item.start }) + showEmpty(it.additional.isEmpty()) + showErrorView(false) + showContent(it.additional.isNotEmpty()) } } - }.afterLoading { - view?.run { - hideRefresh() - showProgress(false) - enableSwipe(true) + .onResourceSuccess { + analytics.logEvent( + "load_data", + "type" to "additional_lessons", + "items" to it.additional.size + ) } - }.launch() + .onResourceNotLoading { + view?.run { + hideRefresh() + showProgress(false) + enableSwipe(true) + } + } + .onResourceError(errorHandler::dispatch) + .launch() } private fun showErrorViewOnError(message: String, error: Throwable) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/AdditionalLessonsView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/AdditionalLessonsView.kt index 97eb2ae7b..76d37b754 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/AdditionalLessonsView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/AdditionalLessonsView.kt @@ -34,5 +34,11 @@ interface AdditionalLessonsView : BaseView { fun showNextButton(show: Boolean) - fun showDatePickerDialog(currentDate: LocalDate) + fun showDatePickerDialog(selectedDate: LocalDate) + + fun showAddAdditionalLessonDialog() + + fun showSuccessMessage() + + fun showDeleteLessonDialog(timetableAdditional: TimetableAdditional) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/add/AdditionalLessonAddDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/add/AdditionalLessonAddDialog.kt new file mode 100644 index 000000000..f82d64830 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/add/AdditionalLessonAddDialog.kt @@ -0,0 +1,172 @@ +package io.github.wulkanowy.ui.modules.timetable.additional.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.timepicker.MaterialTimePicker +import com.google.android.material.timepicker.TimeFormat +import dagger.hilt.android.AndroidEntryPoint +import io.github.wulkanowy.R +import io.github.wulkanowy.databinding.DialogAdditionalAddBinding +import io.github.wulkanowy.ui.base.BaseDialogFragment +import io.github.wulkanowy.utils.lastSchoolDayInSchoolYear +import io.github.wulkanowy.utils.openMaterialDatePicker +import io.github.wulkanowy.utils.toFormattedString +import java.time.LocalDate +import java.time.LocalTime +import javax.inject.Inject + +@AndroidEntryPoint +class AdditionalLessonAddDialog : BaseDialogFragment(), + AdditionalLessonAddView { + + @Inject + lateinit var presenter: AdditionalLessonAddPresenter + + companion object { + fun newInstance() = AdditionalLessonAddDialog() + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setStyle(STYLE_NO_TITLE, 0) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ) = DialogAdditionalAddBinding.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) { + additionalLessonDialogStartEdit.doOnTextChanged { _, _, _, _ -> + additionalLessonDialogStart.isErrorEnabled = false + additionalLessonDialogStart.error = null + } + additionalLessonDialogEndEdit.doOnTextChanged { _, _, _, _ -> + additionalLessonDialogEnd.isErrorEnabled = false + additionalLessonDialogEnd.error = null + } + additionalLessonDialogDateEdit.doOnTextChanged { _, _, _, _ -> + additionalLessonDialogDate.isErrorEnabled = false + additionalLessonDialogDate.error = null + } + additionalLessonDialogContentEdit.doOnTextChanged { _, _, _, _ -> + additionalLessonDialogContent.isErrorEnabled = false + additionalLessonDialogContent.error = null + } + + additionalLessonDialogAdd.setOnClickListener { + presenter.onAddAdditionalClicked( + start = additionalLessonDialogStartEdit.text?.toString(), + end = additionalLessonDialogEndEdit.text?.toString(), + date = additionalLessonDialogDateEdit.text?.toString(), + content = additionalLessonDialogContentEdit.text?.toString(), + isRepeat = additionalLessonDialogRepeat.isChecked + ) + } + additionalLessonDialogClose.setOnClickListener { dismiss() } + additionalLessonDialogDateEdit.setOnClickListener { presenter.showDatePicker() } + additionalLessonDialogStartEdit.setOnClickListener { presenter.showStartTimePicker() } + additionalLessonDialogEndEdit.setOnClickListener { presenter.showEndTimePicker() } + } + } + + override fun showSuccessMessage() { + showMessage(getString(R.string.additional_lessons_add_success)) + } + + override fun setErrorDateRequired() { + with(binding.additionalLessonDialogDate) { + isErrorEnabled = true + error = getString(R.string.error_field_required) + } + } + + override fun setErrorStartRequired() { + with(binding.additionalLessonDialogStart) { + isErrorEnabled = true + error = getString(R.string.error_field_required) + } + } + + override fun setErrorEndRequired() { + with(binding.additionalLessonDialogEnd) { + isErrorEnabled = true + error = getString(R.string.error_field_required) + } + } + + override fun setErrorContentRequired() { + with(binding.additionalLessonDialogContent) { + isErrorEnabled = true + error = getString(R.string.error_field_required) + } + } + + override fun setErrorIncorrectEndTime() { + with(binding.additionalLessonDialogEnd) { + isErrorEnabled = true + error = getString(R.string.additional_lessons_end_time_error) + } + } + + override fun closeDialog() { + dismiss() + } + + override fun showDatePickerDialog(selectedDate: LocalDate) { + openMaterialDatePicker( + selected = selectedDate, + rangeStart = LocalDate.now(), + rangeEnd = LocalDate.now().lastSchoolDayInSchoolYear, + onDateSelected = { + presenter.onDateSelected(it) + binding.additionalLessonDialogDateEdit.setText(it.toFormattedString()) + } + ) + } + + override fun showStartTimePickerDialog(selectedTime: LocalTime) { + showTimePickerDialog(selectedTime) { + presenter.onStartTimeSelected(it) + binding.additionalLessonDialogStartEdit.setText(it.toString()) + } + } + + override fun showEndTimePickerDialog(selectedTime: LocalTime) { + showTimePickerDialog(selectedTime) { + presenter.onEndTimeSelected(it) + binding.additionalLessonDialogEndEdit.setText(it.toString()) + } + } + + private fun showTimePickerDialog(defaultTime: LocalTime, onTimeSelected: (LocalTime) -> Unit) { + val timePicker = MaterialTimePicker.Builder() + .setTimeFormat(TimeFormat.CLOCK_24H) + .setHour(defaultTime.hour) + .setMinute(defaultTime.minute) + .build() + + timePicker.addOnPositiveButtonClickListener { + onTimeSelected(LocalTime.of(timePicker.hour, timePicker.minute)) + } + + if (!parentFragmentManager.isStateSaved) { + timePicker.show(parentFragmentManager, null) + } + } + + override fun onDestroyView() { + presenter.onDetachView() + super.onDestroyView() + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/add/AdditionalLessonAddPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/add/AdditionalLessonAddPresenter.kt new file mode 100644 index 000000000..c207165d3 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/add/AdditionalLessonAddPresenter.kt @@ -0,0 +1,164 @@ +package io.github.wulkanowy.ui.modules.timetable.additional.add + +import io.github.wulkanowy.data.db.entities.TimetableAdditional +import io.github.wulkanowy.data.repositories.SemesterRepository +import io.github.wulkanowy.data.repositories.StudentRepository +import io.github.wulkanowy.data.repositories.TimetableRepository +import io.github.wulkanowy.ui.base.BasePresenter +import io.github.wulkanowy.ui.base.ErrorHandler +import io.github.wulkanowy.utils.lastSchoolDayInSchoolYear +import io.github.wulkanowy.utils.toLocalDate +import kotlinx.coroutines.launch +import timber.log.Timber +import java.time.* +import java.time.temporal.ChronoUnit +import java.util.* +import javax.inject.Inject + +class AdditionalLessonAddPresenter @Inject constructor( + errorHandler: ErrorHandler, + studentRepository: StudentRepository, + private val timetableRepository: TimetableRepository, + private val semesterRepository: SemesterRepository +) : BasePresenter(errorHandler, studentRepository) { + + private var selectedStartTime = LocalTime.of(15, 0) + + private var selectedEndTime = LocalTime.of(15, 45) + + private var selectedDate = LocalDate.now() + + override fun onAttachView(view: AdditionalLessonAddView) { + super.onAttachView(view) + view.initView() + Timber.i("AdditionalLesson details view was initialized") + } + + fun showDatePicker() { + view?.showDatePickerDialog(selectedDate) + } + + fun showStartTimePicker() { + view?.showStartTimePickerDialog(selectedStartTime) + } + + fun showEndTimePicker() { + view?.showEndTimePickerDialog(selectedEndTime) + } + + fun onStartTimeSelected(time: LocalTime) { + selectedStartTime = time + } + + fun onEndTimeSelected(time: LocalTime) { + selectedEndTime = time + } + + fun onDateSelected(date: LocalDate) { + selectedDate = date + } + + fun onAddAdditionalClicked( + start: String?, + end: String?, + date: String?, + content: String?, + isRepeat: Boolean + ) { + if (isUserInputValid(start, end, date, content)) { + addAdditionalLesson( + start = LocalTime.parse(start!!), + end = LocalTime.parse(end), + date = date!!.toLocalDate(), + subject = content!!, + isRepeat = isRepeat + ) + } + } + + private fun isUserInputValid( + start: String?, + end: String?, + date: String?, + content: String? + ): Boolean { + var isValid = true + + if (start.isNullOrBlank()) { + view?.setErrorStartRequired() + isValid = false + } + + if (end.isNullOrBlank()) { + view?.setErrorEndRequired() + isValid = false + } + + if (date.isNullOrBlank()) { + view?.setErrorDateRequired() + isValid = false + } + + if (content.isNullOrBlank()) { + view?.setErrorContentRequired() + isValid = false + } + + if (selectedStartTime >= selectedEndTime) { + view?.setErrorIncorrectEndTime() + isValid = false + } + + return isValid + } + + private fun addAdditionalLesson( + start: LocalTime, + end: LocalTime, + date: LocalDate, + subject: String, + isRepeat: Boolean + ) { + presenterScope.launch { + val semester = runCatching { + val student = studentRepository.getCurrentStudent() + semesterRepository.getCurrentSemester(student) + } + .onFailure(errorHandler::dispatch) + .getOrNull() ?: return@launch + + val weeks = if (isRepeat) { + ChronoUnit.WEEKS.between(date, date.lastSchoolDayInSchoolYear) + } else 0 + val uniqueRepeatId = UUID.randomUUID().takeIf { isRepeat } + + val lessonsToAdd = (0..weeks).map { + TimetableAdditional( + studentId = semester.studentId, + diaryId = semester.diaryId, + start = ZonedDateTime.of(date, start, ZoneId.systemDefault()).toInstant(), + end = ZonedDateTime.of(date, end, ZoneId.systemDefault()).toInstant(), + date = date.plusWeeks(it), + subject = subject + ).apply { + isAddedByUser = true + repeatId = uniqueRepeatId + } + } + + Timber.i("AdditionalLesson insert start") + runCatching { timetableRepository.saveAdditionalList(lessonsToAdd) } + .onSuccess { + Timber.i("AdditionalLesson insert: Success") + view?.run { + showSuccessMessage() + closeDialog() + } + } + .onFailure { + Timber.i("AdditionalLesson insert result: An exception occurred") + errorHandler.dispatch(it) + } + } + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/add/AdditionalLessonAddView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/add/AdditionalLessonAddView.kt new file mode 100644 index 000000000..0df53815b --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/add/AdditionalLessonAddView.kt @@ -0,0 +1,30 @@ +package io.github.wulkanowy.ui.modules.timetable.additional.add + +import io.github.wulkanowy.ui.base.BaseView +import java.time.LocalDate +import java.time.LocalTime + +interface AdditionalLessonAddView : BaseView { + + fun initView() + + fun closeDialog() + + fun showDatePickerDialog(selectedDate: LocalDate) + + fun showStartTimePickerDialog(selectedTime: LocalTime) + + fun showEndTimePickerDialog(selectedTime: LocalTime) + + fun showSuccessMessage() + + fun setErrorDateRequired() + + fun setErrorStartRequired() + + fun setErrorEndRequired() + + fun setErrorContentRequired() + + fun setErrorIncorrectEndTime() +} 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 a6b126447..34a69e6ab 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 @@ -6,8 +6,6 @@ import android.view.View.GONE import android.view.View.INVISIBLE import android.view.View.VISIBLE import androidx.recyclerview.widget.LinearLayoutManager -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.data.db.entities.CompletedLesson @@ -16,14 +14,12 @@ 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.SchoolDaysValidator import io.github.wulkanowy.utils.dpToPx +import io.github.wulkanowy.utils.firstSchoolDayInSchoolYear import io.github.wulkanowy.utils.getCompatDrawable import io.github.wulkanowy.utils.getThemeAttrColor -import io.github.wulkanowy.utils.schoolYearEnd -import io.github.wulkanowy.utils.schoolYearStart -import io.github.wulkanowy.utils.toLocalDateTime -import io.github.wulkanowy.utils.toTimestamp +import io.github.wulkanowy.utils.lastSchoolDayInSchoolYear +import io.github.wulkanowy.utils.openMaterialDatePicker import java.time.LocalDate import javax.inject.Inject @@ -68,9 +64,7 @@ class CompletedLessonsFragment : completedLessonsSwipe.setOnRefreshListener(presenter::onSwipeRefresh) completedLessonsSwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary)) completedLessonsSwipe.setProgressBackgroundColorSchemeColor( - requireContext().getThemeAttrColor( - R.attr.colorSwipeRefresh - ) + requireContext().getThemeAttrColor(R.attr.colorSwipeRefresh) ) completedLessonErrorRetry.setOnClickListener { presenter.onRetry() } completedLessonErrorDetails.setOnClickListener { presenter.onDetailsClick() } @@ -152,30 +146,17 @@ class CompletedLessonsFragment : ) } - override fun showDatePickerDialog(currentDate: LocalDate) { + override fun showDatePickerDialog(selectedDate: LocalDate) { val now = LocalDate.now() - val startOfSchoolYear = now.schoolYearStart.toTimestamp() - val endOfSchoolYear = now.schoolYearEnd.toTimestamp() - val constraintsBuilder = CalendarConstraints.Builder().apply { - setValidator(SchoolDaysValidator(startOfSchoolYear, endOfSchoolYear)) - setStart(startOfSchoolYear) - setEnd(endOfSchoolYear) - } - val datePicker = - MaterialDatePicker.Builder.datePicker() - .setCalendarConstraints(constraintsBuilder.build()) - .setSelection(currentDate.toTimestamp()) - .build() - - datePicker.addOnPositiveButtonClickListener { - val date = it.toLocalDateTime() - presenter.onDateSet(date.year, date.monthValue, date.dayOfMonth) - } - - if (!parentFragmentManager.isStateSaved) { - datePicker.show(parentFragmentManager, null) - } + openMaterialDatePicker( + selected = selectedDate, + rangeStart = now.firstSchoolDayInSchoolYear, + rangeEnd = now.lastSchoolDayInSchoolYear, + onDateSelected = { + presenter.onDateSet(it.year, it.monthValue, it.dayOfMonth) + } + ) } override fun onSaveInstanceState(outState: Bundle) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonsPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonsPresenter.kt index b75b42f8b..16c51fd2e 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonsPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonsPresenter.kt @@ -1,22 +1,13 @@ package io.github.wulkanowy.ui.modules.timetable.completed import android.annotation.SuppressLint -import io.github.wulkanowy.data.Status +import io.github.wulkanowy.data.* import io.github.wulkanowy.data.db.entities.CompletedLesson import io.github.wulkanowy.data.repositories.CompletedLessonsRepository 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.utils.AnalyticsHelper -import io.github.wulkanowy.utils.afterLoading -import io.github.wulkanowy.utils.capitalise -import io.github.wulkanowy.utils.flowWithResourceIn -import io.github.wulkanowy.utils.getLastSchoolDayIfHoliday -import io.github.wulkanowy.utils.isHolidays -import io.github.wulkanowy.utils.nextOrSameSchoolDay -import io.github.wulkanowy.utils.nextSchoolDay -import io.github.wulkanowy.utils.previousSchoolDay -import io.github.wulkanowy.utils.toFormattedString +import io.github.wulkanowy.utils.* import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.onEach @@ -111,51 +102,46 @@ class CompletedLessonsPresenter @Inject constructor( } private fun loadData(forceRefresh: Boolean = false) { - Timber.i("Loading completed lessons data started") - - flowWithResourceIn { + flatResourceFlow { val student = studentRepository.getCurrentStudent() val semester = semesterRepository.getCurrentSemester(student) - completedLessonsRepository.getCompletedLessons(student, semester, currentDate, currentDate, forceRefresh) - }.onEach { - when (it.status) { - Status.LOADING -> { - if (!it.data.isNullOrEmpty()) { - view?.run { - enableSwipe(true) - showRefresh(true) - showProgress(false) - showContent(true) - updateData(it.data.sortedBy { item -> item.number }) - } - } - } - Status.SUCCESS -> { - Timber.i("Loading completed lessons lessons result: Success") - view?.apply { - updateData(it.data!!.sortedBy { item -> item.number }) - showEmpty(it.data.isEmpty()) - showErrorView(false) - showContent(it.data.isNotEmpty()) - } - analytics.logEvent( - "load_data", - "type" to "completed_lessons", - "items" to it.data!!.size - ) - } - Status.ERROR -> { - Timber.i("Loading completed lessons result: An exception occurred") - completedLessonsErrorHandler.dispatch(it.error!!) + completedLessonsRepository.getCompletedLessons( + student = student, + semester = semester, + start = currentDate, + end = currentDate, + forceRefresh = forceRefresh + ) + } + .logResourceStatus("load completed lessons") + .mapResourceData { it.sortedBy { lesson -> lesson.number } } + .onResourceData { + view?.run { + enableSwipe(true) + showProgress(false) + showErrorView(false) + showContent(it.isNotEmpty()) + showEmpty(it.isEmpty()) + updateData(it) } } - }.afterLoading { - view?.run { - showRefresh(false) - showProgress(false) - enableSwipe(true) + .onResourceIntermediate { view?.showRefresh(true) } + .onResourceSuccess { + analytics.logEvent( + "load_data", + "type" to "completed_lessons", + "items" to it.size + ) } - }.launch() + .onResourceNotLoading { + view?.run { + enableSwipe(true) + showProgress(false) + showRefresh(false) + } + } + .onResourceError(errorHandler::dispatch) + .launch() } private fun showErrorViewOnError(message: String, error: Throwable) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonsView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonsView.kt index 7a98874e0..715ce01fc 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonsView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonsView.kt @@ -38,5 +38,5 @@ interface CompletedLessonsView : BaseView { fun showCompletedLessonDialog(completedLesson: CompletedLesson) - fun showDatePickerDialog(currentDate: LocalDate) + fun showDatePickerDialog(selectedDate: LocalDate) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetConfigureActivity.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetConfigureActivity.kt index a27dba882..6ef6cfc98 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetConfigureActivity.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetConfigureActivity.kt @@ -1,8 +1,6 @@ package io.github.wulkanowy.ui.modules.timetablewidget -import android.appwidget.AppWidgetManager.ACTION_APPWIDGET_UPDATE -import android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_ID -import android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_IDS +import android.appwidget.AppWidgetManager.* import android.content.Intent import android.os.Build import android.os.Bundle @@ -17,6 +15,7 @@ import io.github.wulkanowy.databinding.ActivityWidgetConfigureBinding import io.github.wulkanowy.ui.base.BaseActivity import io.github.wulkanowy.ui.base.WidgetConfigureAdapter import io.github.wulkanowy.ui.modules.login.LoginActivity +import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetProvider.Companion.EXTRA_FROM_CONFIGURE import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetProvider.Companion.EXTRA_FROM_PROVIDER import io.github.wulkanowy.utils.AppInfo import javax.inject.Inject @@ -92,6 +91,7 @@ class TimetableWidgetConfigureActivity : .apply { action = ACTION_APPWIDGET_UPDATE putExtra(EXTRA_APPWIDGET_IDS, intArrayOf(widgetId)) + putExtra(EXTRA_FROM_CONFIGURE, true) }) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetConfigurePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetConfigurePresenter.kt index 2a40c8e4a..dc2a7c6c7 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetConfigurePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetConfigurePresenter.kt @@ -1,14 +1,14 @@ package io.github.wulkanowy.ui.modules.timetablewidget -import io.github.wulkanowy.data.Status +import io.github.wulkanowy.data.Resource import io.github.wulkanowy.data.db.SharedPrefProvider import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.repositories.StudentRepository +import io.github.wulkanowy.data.resourceFlow import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetProvider.Companion.getStudentWidgetKey import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetProvider.Companion.getThemeWidgetKey -import io.github.wulkanowy.utils.flowWithResource import kotlinx.coroutines.flow.onEach import timber.log.Timber import javax.inject.Inject @@ -56,16 +56,15 @@ class TimetableWidgetConfigurePresenter @Inject constructor( } private fun loadData() { - flowWithResource { studentRepository.getSavedStudents(false) }.onEach { - when (it.status) { - Status.LOADING -> Timber.d("Timetable widget configure students data load") - Status.SUCCESS -> { + resourceFlow { studentRepository.getSavedStudents(false) }.onEach { + when (it) { + is Resource.Loading -> Timber.d("Timetable widget configure students data load") + is Resource.Success -> { val selectedStudentId = appWidgetId?.let { id -> sharedPref.getLong(getStudentWidgetKey(id), 0) } ?: -1 - when { - it.data!!.isEmpty() -> view?.openLoginView() + it.data.isEmpty() -> view?.openLoginView() it.data.size == 1 && !isFromProvider -> { selectedStudent = it.data.single().student view?.showThemeDialog() @@ -73,7 +72,7 @@ class TimetableWidgetConfigurePresenter @Inject constructor( else -> view?.updateData(it.data, selectedStudentId) } } - Status.ERROR -> errorHandler.dispatch(it.error!!) + is Resource.Error -> errorHandler.dispatch(it.error) } }.launch() } 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 f3d760f09..664086bca 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 @@ -12,6 +12,7 @@ import android.widget.AdapterView.INVALID_POSITION import android.widget.RemoteViews import android.widget.RemoteViewsService import io.github.wulkanowy.R +import io.github.wulkanowy.data.dataOrNull import io.github.wulkanowy.data.db.SharedPrefProvider import io.github.wulkanowy.data.db.entities.Timetable import io.github.wulkanowy.data.enums.TimetableMode @@ -19,11 +20,12 @@ import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.SemesterRepository import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.repositories.TimetableRepository +import io.github.wulkanowy.data.toFirstResult import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetProvider.Companion.getCurrentThemeWidgetKey import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetProvider.Companion.getDateWidgetKey import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetProvider.Companion.getStudentWidgetKey +import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetProvider.Companion.getTodayLastLessonEndDateTimeWidgetKey import io.github.wulkanowy.utils.getCompatColor -import io.github.wulkanowy.utils.toFirstResult import io.github.wulkanowy.utils.toFormattedString import kotlinx.coroutines.runBlocking import timber.log.Timber @@ -70,6 +72,15 @@ class TimetableWidgetFactory( updateTheme(appWidgetId) lessons = getLessons(date, studentId) + + val todayLastLessonEndTimestamp = lessons.maxOfOrNull { it.end } + if (date == LocalDate.now() && todayLastLessonEndTimestamp != null) { + sharedPref.putLong( + key = getTodayLastLessonEndDateTimeWidgetKey(appWidgetId), + value = todayLastLessonEndTimestamp.epochSecond, + sync = true + ) + } } } @@ -108,7 +119,7 @@ class TimetableWidgetFactory( val semester = semesterRepository.getCurrentSemester(student) timetableRepository.getTimetable(student, semester, date, date, false) - .toFirstResult().data?.lessons.orEmpty() + .toFirstResult().dataOrNull?.lessons.orEmpty() .sortedWith(compareBy({ it.number }, { !it.isStudentPlan })) .filter { if (prefRepository.showWholeClassPlan == TimetableMode.ONLY_CURRENT_GROUP) { 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 0f0691160..3ba2ae946 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 @@ -1,12 +1,9 @@ package io.github.wulkanowy.ui.modules.timetablewidget -import android.annotation.SuppressLint import android.app.PendingIntent import android.appwidget.AppWidgetManager -import android.appwidget.AppWidgetManager.ACTION_APPWIDGET_DELETED -import android.appwidget.AppWidgetManager.ACTION_APPWIDGET_UPDATE -import android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_ID -import android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_IDS +import android.appwidget.AppWidgetManager.* +import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK @@ -21,30 +18,21 @@ import io.github.wulkanowy.data.db.SharedPrefProvider import io.github.wulkanowy.data.db.entities.Student 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.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 -import io.github.wulkanowy.utils.nextOrSameSchoolDay -import io.github.wulkanowy.utils.nextSchoolDay -import io.github.wulkanowy.utils.nickOrName -import io.github.wulkanowy.utils.previousSchoolDay -import io.github.wulkanowy.utils.toFormattedString +import io.github.wulkanowy.utils.* import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import timber.log.Timber import java.time.LocalDate -import java.time.LocalDate.now +import java.time.LocalDateTime +import java.time.ZoneOffset import javax.inject.Inject @AndroidEntryPoint -class TimetableWidgetProvider : HiltBroadcastReceiver() { +class TimetableWidgetProvider : BroadcastReceiver() { @Inject lateinit var appWidgetManager: AppWidgetManager @@ -72,10 +60,15 @@ class TimetableWidgetProvider : HiltBroadcastReceiver() { private const val BUTTON_RESET = "buttonReset" + const val EXTRA_FROM_CONFIGURE = "extraFromConfigure" + const val EXTRA_FROM_PROVIDER = "extraFromProvider" fun getDateWidgetKey(appWidgetId: Int) = "timetable_widget_date_$appWidgetId" + fun getTodayLastLessonEndDateTimeWidgetKey(appWidgetId: Int) = + "timetable_widget_today_last_lesson_end_date_time_$appWidgetId" + fun getStudentWidgetKey(appWidgetId: Int) = "timetable_widget_student_$appWidgetId" fun getThemeWidgetKey(appWidgetId: Int) = "timetable_widget_theme_$appWidgetId" @@ -86,7 +79,6 @@ class TimetableWidgetProvider : HiltBroadcastReceiver() { @OptIn(DelicateCoroutinesApi::class) override fun onReceive(context: Context, intent: Intent) { - super.onReceive(context, intent) GlobalScope.launch { when (intent.action) { ACTION_APPWIDGET_UPDATE -> onUpdate(context, intent) @@ -96,11 +88,22 @@ class TimetableWidgetProvider : HiltBroadcastReceiver() { } private suspend fun onUpdate(context: Context, intent: Intent) { - if (intent.getStringExtra(EXTRA_BUTTON_TYPE) === null) { - intent.getIntArrayExtra(EXTRA_APPWIDGET_IDS)?.forEach { appWidgetId -> + if (intent.getStringExtra(EXTRA_BUTTON_TYPE) == null) { + val isFromConfigure = intent.getBooleanExtra(EXTRA_FROM_CONFIGURE, false) + val appWidgetIds = intent.getIntArrayExtra(EXTRA_APPWIDGET_IDS) ?: return + + appWidgetIds.forEach { appWidgetId -> val student = getStudent(sharedPref.getLong(getStudentWidgetKey(appWidgetId), 0), appWidgetId) - updateWidget(context, appWidgetId, now().nextOrSameSchoolDay, student) + val savedDataEpochDay = sharedPref.getLong(getDateWidgetKey(appWidgetId), 0) + + val dateToLoad = if (isFromConfigure && savedDataEpochDay != 0L) { + LocalDate.ofEpochDay(savedDataEpochDay) + } else { + getWidgetDefaultDateToLoad(appWidgetId) + } + + updateWidget(context, appWidgetId, dateToLoad, student) } } else { val buttonType = intent.getStringExtra(EXTRA_BUTTON_TYPE) @@ -112,15 +115,17 @@ class TimetableWidgetProvider : HiltBroadcastReceiver() { val savedDate = LocalDate.ofEpochDay(sharedPref.getLong(getDateWidgetKey(toggledWidgetId), 0)) val date = when (buttonType) { - BUTTON_RESET -> now().nextOrSameSchoolDay + BUTTON_RESET -> getWidgetDefaultDateToLoad(toggledWidgetId) BUTTON_NEXT -> savedDate.nextSchoolDay BUTTON_PREV -> savedDate.previousSchoolDay - else -> now().nextOrSameSchoolDay + else -> getWidgetDefaultDateToLoad(toggledWidgetId) + } + if (!buttonType.isNullOrBlank()) { + analytics.logEvent( + "changed_timetable_widget_day", + "button" to buttonType + ) } - if (!buttonType.isNullOrBlank()) analytics.logEvent( - "changed_timetable_widget_day", - "button" to buttonType - ) updateWidget(context, toggledWidgetId, date, student) } } @@ -138,7 +143,6 @@ class TimetableWidgetProvider : HiltBroadcastReceiver() { } } - @SuppressLint("DefaultLocale") private fun updateWidget( context: Context, appWidgetId: Int, @@ -278,4 +282,21 @@ class TimetableWidgetProvider : HiltBroadcastReceiver() { avatarDrawable.draw(canvas) return avatarBitmap } + + private fun getWidgetDefaultDateToLoad(appWidgetId: Int): LocalDate { + val lastLessonEndTimestamp = + sharedPref.getLong(getTodayLastLessonEndDateTimeWidgetKey(appWidgetId), 0) + val lastLessonEndDateTime = + LocalDateTime.ofEpochSecond(lastLessonEndTimestamp, 0, ZoneOffset.UTC) + + val todayDate = LocalDate.now() + val isLastLessonEndDateNow = lastLessonEndDateTime.toLocalDate() == todayDate + val isLastLessonEndDateAfterNowTime = LocalDateTime.now() > lastLessonEndDateTime + + return if (isLastLessonEndDateNow && isLastLessonEndDateAfterNowTime) { + todayDate.nextSchoolDay + } else { + todayDate.nextOrSameSchoolDay + } + } } 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 ecd982a18..dd91d36d4 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/ContextExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/ContextExtension.kt @@ -1,31 +1,18 @@ package io.github.wulkanowy.utils import android.annotation.SuppressLint -import android.content.ActivityNotFoundException import android.content.Context -import android.content.Intent -import android.graphics.Bitmap -import android.graphics.Color -import android.graphics.Paint -import android.graphics.PorterDuff -import android.graphics.PorterDuffColorFilter -import android.graphics.Rect -import android.graphics.Typeface -import android.net.Uri +import android.graphics.* import android.text.TextPaint import android.util.DisplayMetrics.DENSITY_DEFAULT -import androidx.annotation.AttrRes -import androidx.annotation.ColorInt -import androidx.annotation.ColorRes -import androidx.annotation.DrawableRes -import androidx.annotation.PluralsRes +import androidx.annotation.* import androidx.core.content.ContextCompat import androidx.core.graphics.ColorUtils import androidx.core.graphics.applyCanvas import androidx.core.graphics.drawable.RoundedBitmapDrawable import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory import androidx.core.graphics.drawable.toBitmap -import io.github.wulkanowy.BuildConfig.APPLICATION_ID + @ColorInt fun Context.getThemeAttrColor(@AttrRes colorAttr: Int): Int { @@ -61,69 +48,6 @@ fun Context.getCompatBitmap(@DrawableRes drawableRes: Int, @ColorRes colorRes: I 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 { - startActivity(it) - } catch (e: ActivityNotFoundException) { - onActivityNotFound(uri) - } - } -} - -fun Context.openAppInMarket(onActivityNotFound: (uri: String) -> Unit) { - openInternetBrowser("market://details?id=${APPLICATION_ID}") { - openInternetBrowser("https://github.com/wulkanowy/wulkanowy/releases", onActivityNotFound) - } -} - -fun Context.openEmailClient( - chooserTitle: String, - email: String, - subject: String, - body: String, - onActivityNotFound: () -> Unit = {} -) { - val intent = Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:")).apply { - putExtra(Intent.EXTRA_EMAIL, arrayOf(email)) - putExtra(Intent.EXTRA_SUBJECT, subject) - putExtra(Intent.EXTRA_TEXT, body) - } - - if (intent.resolveActivity(packageManager) != null) { - startActivity(Intent.createChooser(intent, chooserTitle)) - } else onActivityNotFound() -} - -fun Context.openNavigation(location: String) { - val intentUri = Uri.parse("geo:0,0?q=${Uri.encode(location)}") - val intent = Intent(Intent.ACTION_VIEW, intentUri) - if (intent.resolveActivity(packageManager) != null) { - startActivity(intent) - } -} - -fun Context.openDialer(phone: String) { - val intentUri = Uri.parse("tel:$phone") - val intent = Intent(Intent.ACTION_DIAL, intentUri) - if (intent.resolveActivity(packageManager) != null) { - startActivity(intent) - } -} - -fun Context.shareText(text: String, subject: String?) { - val sendIntent: Intent = Intent().apply { - action = Intent.ACTION_SEND - putExtra(Intent.EXTRA_TEXT, text) - if (subject != null) { - putExtra(Intent.EXTRA_SUBJECT, subject) - } - type = "text/plain" - } - val shareIntent = Intent.createChooser(sendIntent, null) - startActivity(shareIntent) -} - fun Context.dpToPx(dp: Float) = dp * resources.displayMetrics.densityDpi / DENSITY_DEFAULT @SuppressLint("DefaultLocale") diff --git a/app/src/main/java/io/github/wulkanowy/utils/ExceptionExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/ExceptionExtension.kt new file mode 100644 index 000000000..43cecd400 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/utils/ExceptionExtension.kt @@ -0,0 +1,74 @@ +package io.github.wulkanowy.utils + +import android.content.res.Resources +import io.github.wulkanowy.R +import io.github.wulkanowy.sdk.exception.FeatureNotAvailableException +import io.github.wulkanowy.sdk.scrapper.exception.FeatureDisabledException +import io.github.wulkanowy.sdk.scrapper.exception.ScrapperException +import io.github.wulkanowy.sdk.scrapper.exception.ServiceUnavailableException +import io.github.wulkanowy.sdk.scrapper.exception.VulcanException +import io.github.wulkanowy.sdk.scrapper.login.NotLoggedInException +import io.github.wulkanowy.sdk.scrapper.login.PasswordChangeRequiredException +import okhttp3.internal.http2.StreamResetException +import java.io.InterruptedIOException +import java.net.ConnectException +import java.net.SocketException +import java.net.SocketTimeoutException +import java.net.UnknownHostException +import java.security.cert.CertificateExpiredException +import java.security.cert.CertificateNotYetValidException +import javax.net.ssl.SSLHandshakeException + +fun Resources.getErrorString(error: Throwable): String = when (error) { + is UnknownHostException -> R.string.error_no_internet + is SocketException, + is SocketTimeoutException, + is InterruptedIOException, + is ConnectException, + is StreamResetException -> R.string.error_timeout + is NotLoggedInException -> R.string.error_login_failed + is PasswordChangeRequiredException -> R.string.error_password_change_required + is ServiceUnavailableException -> R.string.error_service_unavailable + is FeatureDisabledException -> R.string.error_feature_disabled + is FeatureNotAvailableException -> R.string.error_feature_not_available + is VulcanException -> R.string.error_unknown_uonet + is ScrapperException -> R.string.error_unknown_app + is SSLHandshakeException -> when { + error.isCausedByCertificateNotValidNow() -> R.string.error_invalid_device_datetime + else -> R.string.error_timeout + } + else -> R.string.error_unknown +}.let { getString(it) } + +fun Throwable.isShouldBeReported(): Boolean = when (this) { + is UnknownHostException, + is SocketException, + is SocketTimeoutException, + is InterruptedIOException, + is ConnectException, + is StreamResetException, + is ServiceUnavailableException, + is FeatureDisabledException, + is FeatureNotAvailableException -> false + is SSLHandshakeException -> when { + isCausedByCertificateNotValidNow() -> false + else -> true + } + else -> true +} + +private fun Throwable?.isCausedByCertificateNotValidNow(): Boolean { + var exception = this + do { + if (exception.isCertificateNotValidNow()) return true + + exception = exception?.cause + } while (exception != null) + return false +} + +private fun Throwable?.isCertificateNotValidNow(): Boolean { + val isNotYetValid = this is CertificateNotYetValidException + val isExpired = this is CertificateExpiredException + return isNotYetValid || isExpired +} diff --git a/app/src/main/java/io/github/wulkanowy/utils/FlowUtils.kt b/app/src/main/java/io/github/wulkanowy/utils/FlowUtils.kt deleted file mode 100644 index 5dd289677..000000000 --- a/app/src/main/java/io/github/wulkanowy/utils/FlowUtils.kt +++ /dev/null @@ -1,96 +0,0 @@ -package io.github.wulkanowy.utils - -import io.github.wulkanowy.data.Resource -import io.github.wulkanowy.data.Status -import kotlinx.coroutines.FlowPreview -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.flow.emitAll -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.takeWhile -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock - -inline fun networkBoundResource( - mutex: Mutex = Mutex(), - showSavedOnLoading: Boolean = true, - crossinline query: () -> Flow, - crossinline fetch: suspend (ResultType) -> RequestType, - crossinline saveFetchResult: suspend (old: ResultType, new: RequestType) -> Unit, - crossinline onFetchFailed: (Throwable) -> Unit = { }, - crossinline shouldFetch: (ResultType) -> Boolean = { true }, - crossinline filterResult: (ResultType) -> ResultType = { it } -) = flow { - emit(Resource.loading()) - - val data = query().first() - emitAll(if (shouldFetch(data)) { - if (showSavedOnLoading) emit(Resource.loading(filterResult(data))) - - try { - val newData = fetch(data) - mutex.withLock { saveFetchResult(query().first(), newData) } - query().map { Resource.success(filterResult(it)) } - } catch (throwable: Throwable) { - onFetchFailed(throwable) - query().map { Resource.error(throwable, filterResult(it)) } - } - } else { - query().map { Resource.success(filterResult(it)) } - }) -} - -@JvmName("networkBoundResourceWithMap") -inline fun networkBoundResource( - mutex: Mutex = Mutex(), - showSavedOnLoading: Boolean = true, - crossinline query: () -> Flow, - crossinline fetch: suspend (ResultType) -> RequestType, - crossinline saveFetchResult: suspend (old: ResultType, new: RequestType) -> Unit, - crossinline onFetchFailed: (Throwable) -> Unit = { }, - crossinline shouldFetch: (ResultType) -> Boolean = { true }, - crossinline mapResult: (ResultType) -> T -) = flow { - emit(Resource.loading()) - - val data = query().first() - emitAll(if (shouldFetch(data)) { - if (showSavedOnLoading) emit(Resource.loading(mapResult(data))) - - try { - val newData = fetch(data) - mutex.withLock { saveFetchResult(query().first(), newData) } - query().map { Resource.success(mapResult(it)) } - } catch (throwable: Throwable) { - onFetchFailed(throwable) - query().map { Resource.error(throwable, mapResult(it)) } - } - } else { - query().map { Resource.success(mapResult(it)) } - }) -} - -fun flowWithResource(block: suspend () -> T) = flow { - emit(Resource.loading()) - emit(Resource.success(block())) -}.catch { emit(Resource.error(it)) } - -@OptIn(FlowPreview::class) -fun flowWithResourceIn(block: suspend () -> Flow>) = flow { - emit(Resource.loading()) - emitAll(block().filter { it.status != Status.LOADING || (it.status == Status.LOADING && it.data != null) }) -}.catch { emit(Resource.error(it)) } - -fun Flow>.afterLoading(callback: () -> Unit) = onEach { - if (it.status != Status.LOADING) callback() -} - -suspend fun Flow>.toFirstResult() = filter { it.status != Status.LOADING }.first() - -suspend fun Flow>.waitForResult() = - takeWhile { it.status == Status.LOADING }.collect() diff --git a/app/src/main/java/io/github/wulkanowy/utils/IntentUtils.kt b/app/src/main/java/io/github/wulkanowy/utils/IntentUtils.kt new file mode 100644 index 000000000..1ef03f2e6 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/utils/IntentUtils.kt @@ -0,0 +1,100 @@ +package io.github.wulkanowy.utils + +import android.content.ActivityNotFoundException +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.provider.CalendarContract +import io.github.wulkanowy.BuildConfig +import java.time.LocalDateTime +import java.time.ZoneId + +fun Context.openInternetBrowser(uri: String, onActivityNotFound: (uri: String) -> Unit = {}) { + Intent.parseUri(uri, 0).let { + try { + startActivity(it) + } catch (e: ActivityNotFoundException) { + onActivityNotFound(uri) + } + } +} + +fun Context.openAppInMarket(onActivityNotFound: (uri: String) -> Unit) { + openInternetBrowser("market://details?id=${BuildConfig.APPLICATION_ID}") { + openInternetBrowser("https://github.com/wulkanowy/wulkanowy/releases", onActivityNotFound) + } +} + +fun Context.openEmailClient( + chooserTitle: String, + email: String, + subject: String, + body: String, + onActivityNotFound: () -> Unit = {} +) { + val intent = Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:")).apply { + putExtra(Intent.EXTRA_EMAIL, arrayOf(email)) + putExtra(Intent.EXTRA_SUBJECT, subject) + putExtra(Intent.EXTRA_TEXT, body) + } + + if (intent.resolveActivity(packageManager) != null) { + startActivity(Intent.createChooser(intent, chooserTitle)) + } else onActivityNotFound() +} + +fun Context.openCalendarEventAdd( + title: String, + description: String, + start: LocalDateTime, + end: LocalDateTime? = null, + isAllDay: Boolean = false, + onActivityNotFound: (uri: String?) -> Unit = {}, +) { + val beginTime = start.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli() + val endTime = end?.atZone(ZoneId.systemDefault())?.toInstant()?.toEpochMilli() + + val intent = Intent(Intent.ACTION_INSERT) + .setData(CalendarContract.Events.CONTENT_URI) + .putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, beginTime) + .putExtra(CalendarContract.EXTRA_EVENT_END_TIME, endTime) + .putExtra(CalendarContract.EXTRA_EVENT_ALL_DAY, isAllDay) + .putExtra(CalendarContract.Events.TITLE, title) + .putExtra(CalendarContract.Events.DESCRIPTION, description) + .putExtra(CalendarContract.Events.AVAILABILITY, CalendarContract.Events.AVAILABILITY_BUSY) + + try { + startActivity(intent) + } catch (e: ActivityNotFoundException) { + onActivityNotFound(intent.dataString) + } +} + +fun Context.openNavigation(location: String) { + val intentUri = Uri.parse("geo:0,0?q=${Uri.encode(location)}") + val intent = Intent(Intent.ACTION_VIEW, intentUri) + if (intent.resolveActivity(packageManager) != null) { + startActivity(intent) + } +} + +fun Context.openDialer(phone: String) { + val intentUri = Uri.parse("tel:$phone") + val intent = Intent(Intent.ACTION_DIAL, intentUri) + if (intent.resolveActivity(packageManager) != null) { + startActivity(intent) + } +} + +fun Context.shareText(text: String, subject: String?) { + val sendIntent: Intent = Intent().apply { + action = Intent.ACTION_SEND + putExtra(Intent.EXTRA_TEXT, text) + if (subject != null) { + putExtra(Intent.EXTRA_SUBJECT, subject) + } + type = "text/plain" + } + val shareIntent = Intent.createChooser(sendIntent, null) + startActivity(shareIntent) +} diff --git a/app/src/main/java/io/github/wulkanowy/utils/MaterialDatePickerUtils.kt b/app/src/main/java/io/github/wulkanowy/utils/MaterialDatePickerUtils.kt new file mode 100644 index 000000000..09ccda899 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/utils/MaterialDatePickerUtils.kt @@ -0,0 +1,50 @@ +package io.github.wulkanowy.utils + +import androidx.fragment.app.Fragment +import com.google.android.material.datepicker.CalendarConstraints +import com.google.android.material.datepicker.MaterialDatePicker +import kotlinx.parcelize.Parcelize +import java.time.LocalDate +import java.time.temporal.ChronoUnit + +fun Fragment.openMaterialDatePicker( + selected: LocalDate, + rangeStart: LocalDate, + rangeEnd: LocalDate, + onDateSelected: (LocalDate) -> Unit, +) { + val constraintsBuilder = CalendarConstraints.Builder().apply { + setValidator(CalendarDayRangeValidator(rangeStart, rangeEnd)) + setStart(rangeStart.toTimestamp()) + setEnd(rangeEnd.toTimestamp()) + } + + val datePicker = MaterialDatePicker.Builder.datePicker() + .setCalendarConstraints(constraintsBuilder.build()) + .setSelection(selected.toTimestamp()) + .build() + + datePicker.addOnPositiveButtonClickListener { + val date = it.toLocalDateTime().toLocalDate() + onDateSelected(date) + } + + if (!parentFragmentManager.isStateSaved) { + datePicker.show(parentFragmentManager, null) + } +} + +@Parcelize +private class CalendarDayRangeValidator( + val start: LocalDate, + val end: LocalDate, +) : CalendarConstraints.DateValidator { + + override fun isValid(dateLong: Long): Boolean { + val date = dateLong.toLocalDateTime().toLocalDate() + val daysUntilEnd = date.until(end, ChronoUnit.DAYS) + val daysUntilStart = date.until(start, ChronoUnit.DAYS) + + return daysUntilStart <= 0 && daysUntilEnd >= 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 6bf97bae7..c69fec65c 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/RefreshUtils.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/RefreshUtils.kt @@ -8,8 +8,9 @@ import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.enums.MessageFolder import timber.log.Timber +import java.time.Duration.ofMinutes +import java.time.Instant import java.time.LocalDate -import java.time.LocalDateTime import javax.inject.Inject fun getRefreshKey(name: String, semester: Semester, start: LocalDate, end: LocalDate): String { @@ -34,10 +35,10 @@ class AutoRefreshHelper @Inject constructor( ) { fun shouldBeRefreshed(key: String): Boolean { - val timestamp = sharedPref.getLong(key, 0).toLocalDateTime() + val timestamp = sharedPref.getLong(key, 0).let(Instant::ofEpochMilli) val servicesInterval = sharedPref.getString(context.getString(R.string.pref_key_services_interval), context.getString(R.string.pref_default_services_interval)).toLong() - val shouldBeRefreshed = timestamp < LocalDateTime.now().minusMinutes(servicesInterval) + val shouldBeRefreshed = timestamp < Instant.now().minus(ofMinutes(servicesInterval)) Timber.d("Check if $key need to be refreshed: $shouldBeRefreshed (last refresh: $timestamp, interval: $servicesInterval min)") @@ -45,6 +46,6 @@ class AutoRefreshHelper @Inject constructor( } fun updateLastRefreshTimestamp(key: String) { - sharedPref.putLong(key, LocalDateTime.now().toTimestamp()) + sharedPref.putLong(key, Instant.now().toEpochMilli()) } } diff --git a/app/src/main/java/io/github/wulkanowy/utils/ResourcesExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/ResourcesExtension.kt deleted file mode 100644 index da5fd3dbb..000000000 --- a/app/src/main/java/io/github/wulkanowy/utils/ResourcesExtension.kt +++ /dev/null @@ -1,29 +0,0 @@ -package io.github.wulkanowy.utils - -import android.content.res.Resources -import io.github.wulkanowy.R -import io.github.wulkanowy.sdk.exception.FeatureNotAvailableException -import io.github.wulkanowy.sdk.scrapper.exception.FeatureDisabledException -import io.github.wulkanowy.sdk.scrapper.exception.ScrapperException -import io.github.wulkanowy.sdk.scrapper.exception.ServiceUnavailableException -import io.github.wulkanowy.sdk.scrapper.exception.VulcanException -import io.github.wulkanowy.sdk.scrapper.login.NotLoggedInException -import io.github.wulkanowy.sdk.scrapper.login.PasswordChangeRequiredException -import okhttp3.internal.http2.StreamResetException -import java.io.InterruptedIOException -import java.net.ConnectException -import java.net.SocketTimeoutException -import java.net.UnknownHostException - -fun Resources.getString(error: Throwable) = when (error) { - is UnknownHostException -> getString(R.string.error_no_internet) - is SocketTimeoutException, is InterruptedIOException, is ConnectException, is StreamResetException -> getString(R.string.error_timeout) - is NotLoggedInException -> getString(R.string.error_login_failed) - is PasswordChangeRequiredException -> getString(R.string.error_password_change_required) - is ServiceUnavailableException -> getString(R.string.error_service_unavailable) - is FeatureDisabledException -> getString(R.string.error_feature_disabled) - is FeatureNotAvailableException -> getString(R.string.error_feature_not_available) - is VulcanException -> getString(R.string.error_unknown_uonet) - is ScrapperException -> getString(R.string.error_unknown_app) - else -> getString(R.string.error_unknown) -} diff --git a/app/src/main/java/io/github/wulkanowy/utils/SchooldaysValidator.kt b/app/src/main/java/io/github/wulkanowy/utils/SchooldaysValidator.kt deleted file mode 100644 index b6dd528f5..000000000 --- a/app/src/main/java/io/github/wulkanowy/utils/SchooldaysValidator.kt +++ /dev/null @@ -1,16 +0,0 @@ -package io.github.wulkanowy.utils - -import com.google.android.material.datepicker.CalendarConstraints -import kotlinx.parcelize.Parcelize -import java.time.temporal.ChronoUnit - -@Parcelize -class SchoolDaysValidator(val start: Long, val end: Long) : CalendarConstraints.DateValidator { - - override fun isValid(dateLong: Long): Boolean { - val date = dateLong.toLocalDateTime() - - return date.until(end.toLocalDateTime(), ChronoUnit.DAYS) >= 0 && - date.until(start.toLocalDateTime(), ChronoUnit.DAYS) <= 0 - } -} diff --git a/app/src/main/java/io/github/wulkanowy/utils/SemesterExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/SemesterExtension.kt index 6e11a8b2c..380d6bf6e 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/SemesterExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/SemesterExtension.kt @@ -15,5 +15,8 @@ fun List.getCurrentOrLast(): Semester { // when there is more than one current semester - find one with higher id singleOrNull { semester -> semester.semesterId == maxByOrNull { it.semesterId }?.semesterId }?.let { return it } + // when there is no active kindergarten semester - get one from last year + singleOrNull { semester -> semester.schoolYear == maxByOrNull { it.schoolYear }?.schoolYear }?.let { return it } + throw IllegalArgumentException("Duplicated last semester! Semesters: ${joinToString(separator = "\n")}") } diff --git a/app/src/main/java/io/github/wulkanowy/utils/TimeExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/TimeExtension.kt index 94b6a2191..e7a50d0c3 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/TimeExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/TimeExtension.kt @@ -1,41 +1,34 @@ package io.github.wulkanowy.utils import java.text.SimpleDateFormat -import java.time.DayOfWeek.FRIDAY -import java.time.DayOfWeek.MONDAY -import java.time.DayOfWeek.SATURDAY -import java.time.DayOfWeek.SUNDAY -import java.time.Instant -import java.time.LocalDate -import java.time.LocalDateTime -import java.time.LocalTime -import java.time.Month -import java.time.ZoneId -import java.time.ZoneOffset +import java.time.* +import java.time.DayOfWeek.* import java.time.format.DateTimeFormatter -import java.time.temporal.TemporalAdjusters.firstInMonth -import java.time.temporal.TemporalAdjusters.next -import java.time.temporal.TemporalAdjusters.previous -import java.util.Locale +import java.time.temporal.TemporalAdjusters.* +import java.util.* private const val DEFAULT_DATE_PATTERN = "dd.MM.yyyy" +fun LocalDate.toTimestamp(): Long = atStartOfDay() + .toInstant(ZoneOffset.UTC) + .toEpochMilli() + +fun Long.toLocalDateTime(): LocalDateTime = LocalDateTime.ofInstant( + Instant.ofEpochMilli(this), ZoneOffset.UTC +) + +fun Instant.toLocalDate(): LocalDate = atZone(ZoneOffset.UTC).toLocalDate() + fun String.toLocalDate(format: String = DEFAULT_DATE_PATTERN): LocalDate = LocalDate.parse(this, DateTimeFormatter.ofPattern(format)) -fun LocalDateTime.toTimestamp() = - atZone(ZoneId.systemDefault()).withZoneSameInstant(ZoneOffset.UTC).toInstant().toEpochMilli() - -fun Long.toLocalDateTime(): LocalDateTime = - LocalDateTime.ofInstant(Instant.ofEpochMilli(this), ZoneId.systemDefault()) - -fun LocalDate.toTimestamp() = atTime(LocalTime.now()).toTimestamp() - fun LocalDate.toFormattedString(pattern: String = DEFAULT_DATE_PATTERN): String = format(DateTimeFormatter.ofPattern(pattern)) -fun LocalDateTime.toFormattedString(pattern: String = DEFAULT_DATE_PATTERN): String = - format(DateTimeFormatter.ofPattern(pattern)) +fun Instant.toFormattedString( + pattern: String = DEFAULT_DATE_PATTERN, + tz: ZoneId = ZoneId.systemDefault() +): String = atZone(tz).format(DateTimeFormatter.ofPattern(pattern)) fun Month.getFormattedName(): String { val formatter = SimpleDateFormat("LLLL", Locale.getDefault()) @@ -85,35 +78,31 @@ inline val LocalDate.previousOrSameSchoolDay: LocalDate inline val LocalDate.weekDayName: String get() = format(DateTimeFormatter.ofPattern("EEEE", Locale.getDefault())) -inline val LocalDate.monday: LocalDate - get() = with(MONDAY) +inline val LocalDate.monday: LocalDate get() = with(MONDAY) -inline val LocalDate.sunday: LocalDate - get() = with(SUNDAY) +inline val LocalDate.sunday: LocalDate get() = with(SUNDAY) /** * [Dz.U. 2016 poz. 1335](http://prawo.sejm.gov.pl/isap.nsf/DocDetails.xsp?id=WDU20160001335) */ -inline val LocalDate.isHolidays: Boolean - get() = isBefore(firstSchoolDay) && isAfter(lastSchoolDay) +val LocalDate.isHolidays: Boolean + get() = isBefore(firstSchoolDayInCalendarYear) && isAfter(lastSchoolDayInCalendarYear) -inline val LocalDate.firstSchoolDay: LocalDate - get() = LocalDate.of(year, 9, 1).run { - when (dayOfWeek) { - FRIDAY, SATURDAY, SUNDAY -> with(firstInMonth(MONDAY)) - else -> this - } +val LocalDate.firstSchoolDayInSchoolYear: LocalDate + get() = withYear(if (this.monthValue <= 6) this.year - 1 else this.year).firstSchoolDayInCalendarYear + +val LocalDate.lastSchoolDayInSchoolYear: LocalDate + get() = withYear(if (this.monthValue > 6) this.year + 1 else this.year).lastSchoolDayInCalendarYear + +fun LocalDate.getLastSchoolDayIfHoliday(schoolYear: Int): LocalDate { + val date = LocalDate.of(schoolYear.getSchoolYearByMonth(monthValue), monthValue, dayOfMonth) + + if (date.isHolidays) { + return date.lastSchoolDayInCalendarYear } -inline val LocalDate.lastSchoolDay: LocalDate - get() = LocalDate.of(year, 6, 20) - .with(next(FRIDAY)) - -inline val LocalDate.schoolYearStart: LocalDate - get() = withYear(if (this.monthValue <= 6) this.year - 1 else this.year).firstSchoolDay - -inline val LocalDate.schoolYearEnd: LocalDate - get() = withYear(if (this.monthValue > 6) this.year + 1 else this.year).lastSchoolDay + return date +} private fun Int.getSchoolYearByMonth(monthValue: Int): Int { return when (monthValue) { @@ -122,12 +111,15 @@ private fun Int.getSchoolYearByMonth(monthValue: Int): Int { } } -fun LocalDate.getLastSchoolDayIfHoliday(schoolYear: Int): LocalDate { - val date = LocalDate.of(schoolYear.getSchoolYearByMonth(monthValue), monthValue, dayOfMonth) - - if (date.isHolidays) { - return date.lastSchoolDay +private inline val LocalDate.firstSchoolDayInCalendarYear: LocalDate + get() = LocalDate.of(year, 9, 1).run { + when (dayOfWeek) { + FRIDAY, SATURDAY, SUNDAY -> with(firstInMonth(MONDAY)) + else -> this + } } - return date -} +private inline val LocalDate.lastSchoolDayInCalendarYear: LocalDate + get() = LocalDate.of(year, 6, 20) + .with(next(FRIDAY)) + diff --git a/app/src/main/java/io/github/wulkanowy/utils/TimetableExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/TimetableExtension.kt index 9d15216c6..3e94463b9 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/TimetableExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/TimetableExtension.kt @@ -3,10 +3,10 @@ package io.github.wulkanowy.utils import io.github.wulkanowy.data.db.entities.Timetable import java.time.Duration import java.time.Duration.between -import java.time.LocalDateTime -import java.time.LocalDateTime.now +import java.time.Instant +import java.time.Instant.now -fun Timetable.isShowTimeUntil(previousLessonEnd: LocalDateTime?) = when { +fun Timetable.isShowTimeUntil(previousLessonEnd: Instant?) = when { !isStudentPlan -> false canceled -> false now().isAfter(start) -> false 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 f40e3fc33..064401abd 100644 --- a/app/src/main/play/release-notes/pl-PL/default.txt +++ b/app/src/main/play/release-notes/pl-PL/default.txt @@ -1,6 +1,7 @@ -Wersja 1.4.4 +Wersja 1.7.5 -- naprawiliśmy logowanie do Gdańskiej Platformy Edukacyjnej -- naprawiliśmy sortowanie ocen oraz ogłoszeń +- naprawiliśmy kilka błędów w obsłudze nowego modułu wiadomości +- naprawiliśmy wyświetlanie napisu "Brak ocen", jesli uczeń nie zdobył w danym semestrze jeszcze żadnych ocen +- naprawiliśmy logowanie do aplikacji rodzicom będącym jednocześnie nauczycielami Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases diff --git a/app/src/main/res/drawable/ic_all_clock.xml b/app/src/main/res/drawable/ic_all_clock.xml new file mode 100644 index 000000000..4b98ed233 --- /dev/null +++ b/app/src/main/res/drawable/ic_all_clock.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_calendar_all.xml b/app/src/main/res/drawable/ic_calendar_all.xml new file mode 100644 index 000000000..5908035ed --- /dev/null +++ b/app/src/main/res/drawable/ic_calendar_all.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_message_select_all.xml b/app/src/main/res/drawable/ic_message_select_all.xml new file mode 100644 index 000000000..eab195d94 --- /dev/null +++ b/app/src/main/res/drawable/ic_message_select_all.xml @@ -0,0 +1,17 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_message_unselect_all.xml b/app/src/main/res/drawable/ic_message_unselect_all.xml new file mode 100644 index 000000000..c388522e4 --- /dev/null +++ b/app/src/main/res/drawable/ic_message_unselect_all.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml index 1d5b5280d..912792638 100644 --- a/app/src/main/res/layout/activity_login.xml +++ b/app/src/main/res/layout/activity_login.xml @@ -1,5 +1,5 @@ @@ -10,8 +10,11 @@ android:layout_height="wrap_content" android:background="@android:color/transparent" /> - + android:layout_height="0dp" + android:layout_weight="1" + tools:layout="@layout/fragment_login_form" /> + diff --git a/app/src/main/res/layout/activity_send_message.xml b/app/src/main/res/layout/activity_send_message.xml index 10b581f77..320782bdc 100644 --- a/app/src/main/res/layout/activity_send_message.xml +++ b/app/src/main/res/layout/activity_send_message.xml @@ -16,8 +16,7 @@ app:layout_constraintBottom_toTopOf="@id/sendMessageScroll" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" - tools:targetApi="lollipop" /> + app:layout_constraintTop_toTopOf="parent" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_ads_consent.xml b/app/src/main/res/layout/dialog_ads_consent.xml new file mode 100644 index 000000000..816074783 --- /dev/null +++ b/app/src/main/res/layout/dialog_ads_consent.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_error.xml b/app/src/main/res/layout/dialog_error.xml index b52c99aca..98b9c8b16 100644 --- a/app/src/main/res/layout/dialog_error.xml +++ b/app/src/main/res/layout/dialog_error.xml @@ -7,15 +7,6 @@ android:orientation="vertical" tools:context=".ui.base.ErrorDialog"> - - - + android:layout_height="200dp" + android:overScrollMode="ifContentScrolls" + android:paddingHorizontal="24dp" + app:layout_constraintTop_toTopOf="parent"> - + android:layout_height="wrap_content"> - - - - - - - - - - - - - - - - - - - + + + diff --git a/app/src/main/res/layout/dialog_exam.xml b/app/src/main/res/layout/dialog_exam.xml index 51153ac8e..0d04b1fac 100644 --- a/app/src/main/res/layout/dialog_exam.xml +++ b/app/src/main/res/layout/dialog_exam.xml @@ -1,13 +1,15 @@ + android:layout_height="match_parent" + tools:context=".ui.modules.exam.ExamDialog"> + app:layout_constraintTop_toBottomOf="@id/examDialogDeadlineDateTitle" /> + + + + + app:layout_constraintTop_toBottomOf="@id/examDialogEntryDateValue" /> + + - diff --git a/app/src/main/res/layout/dialog_homework_add.xml b/app/src/main/res/layout/dialog_homework_add.xml index b9b8d1a2e..524f0db0d 100644 --- a/app/src/main/res/layout/dialog_homework_add.xml +++ b/app/src/main/res/layout/dialog_homework_add.xml @@ -5,8 +5,8 @@ android:layout_height="match_parent" android:fillViewport="true" android:minWidth="300dp" - android:paddingStart="24dp" - android:paddingEnd="24dp"> + android:paddingStart="8dp" + android:paddingEnd="8dp"> + app:startIconDrawable="@drawable/ic_calendar_all"> @@ -66,7 +66,7 @@ style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginStart="0dp" + android:layout_marginHorizontal="16dp" android:layout_marginTop="16dp" android:hint="@string/all_teacher"> @@ -81,7 +81,7 @@ style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginStart="0dp" + android:layout_marginHorizontal="16dp" android:layout_marginTop="16dp" android:layout_marginBottom="16dp" android:hint="@string/all_content"> @@ -105,13 +105,12 @@ android:layout_gravity="center_vertical" android:layout_marginStart="0dp" android:layout_marginLeft="0dp" - android:layout_marginTop="8dp" - android:layout_marginEnd="8dp" android:layout_marginBottom="8dp" android:insetLeft="0dp" android:insetTop="0dp" android:insetRight="0dp" android:insetBottom="0dp" + android:minWidth="88dp" android:text="@string/all_close" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" /> @@ -126,9 +125,10 @@ android:insetTop="0dp" android:insetRight="0dp" android:insetBottom="0dp" + android:minWidth="88dp" android:text="@string/all_add" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" /> - \ No newline at end of file + diff --git a/app/src/main/res/layout/fragment_login_recover.xml b/app/src/main/res/layout/fragment_login_recover.xml index 76bdad228..028d6bc1f 100644 --- a/app/src/main/res/layout/fragment_login_recover.xml +++ b/app/src/main/res/layout/fragment_login_recover.xml @@ -155,7 +155,7 @@ android:orientation="vertical" android:visibility="invisible" tools:ignore="UseCompoundDrawables" - tools:visibility="visible"> + tools:visibility="gone"> + tools:listitem="@layout/item_message_preview" + tools:visibility="visible" /> + android:layout_height="match_parent" + tools:itemCount="4" + tools:listitem="@layout/item_note" /> + tools:visibility="gone"> + tools:visibility="gone"> @@ -108,6 +110,18 @@ android:text="@string/all_retry" /> + + + + + + + diff --git a/app/src/main/res/layout/item_message.xml b/app/src/main/res/layout/item_message.xml index 111de88c9..c25faacc8 100644 --- a/app/src/main/res/layout/item_message.xml +++ b/app/src/main/res/layout/item_message.xml @@ -5,22 +5,34 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:background="?selectableItemBackground" - android:paddingLeft="16dp" android:paddingTop="10dp" - android:paddingRight="16dp" + android:paddingEnd="16dp" android:paddingBottom="10dp" tools:context=".ui.modules.message.tab.MessageTabAdapter"> + + @@ -45,6 +57,7 @@ android:textColor="?android:textColorSecondary" android:textSize="12sp" app:layout_constraintEnd_toStartOf="@id/messageItemAttachmentIcon" + app:layout_constraintStart_toEndOf="@id/messageItemCheckbox" app:layout_constraintStart_toStartOf="@id/messageItemAuthor" app:layout_constraintTop_toBottomOf="@+id/messageItemAuthor" app:layout_goneMarginEnd="0dp" diff --git a/app/src/main/res/layout/item_note.xml b/app/src/main/res/layout/item_note.xml index 660f32e00..aa1949892 100644 --- a/app/src/main/res/layout/item_note.xml +++ b/app/src/main/res/layout/item_note.xml @@ -54,7 +54,7 @@ + android:focusable="true" + android:minHeight="64dp"> + tools:maxLines="2" + tools:text="@tools:sample/lorem/random" /> + diff --git a/app/src/main/res/menu/action_menu_message_preview.xml b/app/src/main/res/menu/action_menu_message_preview.xml index 4c1332e10..5011e2356 100644 --- a/app/src/main/res/menu/action_menu_message_preview.xml +++ b/app/src/main/res/menu/action_menu_message_preview.xml @@ -19,7 +19,7 @@ android:id="@+id/messagePreviewMenuDelete" android:icon="@drawable/ic_menu_message_delete" android:orderInCategory="1" - android:title="@string/message_delete" + android:title="@string/message_move_to_trash" app:iconTint="@color/material_on_surface_emphasis_medium" app:showAsAction="ifRoom" /> +

+ + + diff --git a/app/src/main/res/values-cs/preferences_values.xml b/app/src/main/res/values-cs/preferences_values.xml index 5252f79b8..23073adf1 100644 --- a/app/src/main/res/values-cs/preferences_values.xml +++ b/app/src/main/res/values-cs/preferences_values.xml @@ -34,6 +34,7 @@ Abecedně Podle data + Podle průměru Dzienniczek+ diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 93ea10a11..d036e7e42 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -12,7 +12,7 @@ O aplikaci Prohlížeč protokolů Ladění - Ladění upozornění + Ladění oznámení Tvůrci Licence Zprávy @@ -25,7 +25,7 @@ Podrobnosti účtu Informace o žáku Domů - Centrum upozornění + Centrum oznámení Semestr %1$d, %2$d/%3$d @@ -55,7 +55,7 @@ Neplatný symbol Žák nebyl nalezen. Zkontrolujte správnost symbolu a vybrané varianty deníku UONET+ 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 + 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+ 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í frekvencí, ospravedlnění nepřítomnosti, dokončené lekce, informace o škole a prohlížení seznamu registrovaných zařízení @@ -71,11 +71,15 @@ Obnovte svůj účet Obnovit Žák je už přihlášen + Standardní Manažer účtů Přihlásit se Relace vypršela Relace vypršela. Přihlaste se prosím znovu + Podpora aplikace + Líbí se Vám tato aplikace? Podpořte její vývoj tím, že povolíte neinvazivní reklamy, které můžete kdykoliv vypnout + Zapnout reklamy Známka Semestr %d @@ -93,9 +97,9 @@ 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ů + Vypočítaný průměr je aritmetický průměr vypočítaný z průměrů předmětů. Umožňuje vám to znát přibližný konečný průměr. Vypočítává se způsobem zvoleným uživatelem v nastavení aplikaci. Doporučuje se vybrat příslušnou možnost. Důvodem je rozdílný výpočet školních průměrů. Pokud vaše škola navíc uvádí průměr předmětů na stránce deníku Vulcan, aplikace si je stáhne a tyto průměry nepočítá. To lze změnit vynucením výpočtu průměru v nastavení aplikaci.\n\nPrůměr známek pouze z vybraného semestru:\n1. Výpočet váženého průměru pro každý předmět v daném semestru\n2. Sčítání vypočítaných průměrů\n3. Výpočet aritmetického průměru součtených průměrů\n\nPrůměr průměrů z obou semestrů:\n1. Výpočet váženého průměru pro každý předmět v semestru 1 a 2\n2. Výpočet aritmetického průměru vypočítaných průměrů za semestry 1 a 2 pro každý předmět.\n3. Sčítání vypočítaných průměrů\n4. Výpočet aritmetického průměru sečtených průměrů\n\nPrůměr známek z celého roku:\n1. Výpočet váženého průměru za rok pro každý předmět. Konečný průměr v 1. semestru je nepodstatný.\n2. Sčítání vypočítaných průměrů\n3. Výpočet aritmetického průměru součtených průměrů 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 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 uděleny známky Konečný průměr z %1$d z %2$d předmětů Shrnutí @@ -105,7 +109,9 @@ Semestr Body Vysvětlivky - Průměr: %1$s + Průměr třídy: %1$s + Váš průměr: %1$s + Vaše známka: %1$s Třída Žák @@ -204,9 +210,20 @@ Další lekce Zobrazit další lekce Žádné informace o dalších lekcích + Nová lekce + Nová další lekce + Další lekce byla úspěšně přidána + Další lekce byla úspěšně odstraněna + Opakovat každý týden + Odstranit další lekci + Pouze tato lekce + Všechny v sérii + Čas zahájení + Čas ukončení + Čas ukončení musí být pozdější než čas zahájení Shrnutí frekvencí - Neprítomnosť zo školských dôvodov + Nepřítomnost ze školních důvodů Omluvená nepřítomnost Neomluvená nepřítomnost Osvobození @@ -253,10 +270,10 @@ Nové zkoušky - Máte %d novou zkoušku - Máte %d nové zkoušky - Máte %d nových zkoušek - Máte %d nových zkoušek + %d nová zkouška + %d nové zkoušky + %d nových zkoušek + %d nových zkoušek %d zkouška @@ -272,13 +289,18 @@ Žádné zprávy Od: Komu: - Datum: %s + Datum: %1$s Odpověď Poslat dále - Odstranit - Přesunout do koše + Vybrat vše + Odznačit vše + Přesunout do koše Odstranit natrvalo Zpráva byla úspěšně odstraněna + žák + rodič + opatrovník + pracovník Sdílet Vytisknout Předmět @@ -290,12 +312,11 @@ Pouze nepřečtené Pouze s přílohami Přečtena: %s - Přečtena přes: %1$d z %2$d osob - %d zpráva - %d zprávy - %d zpráv - %d zpráv + %1$d zpráva + %1$d zprávy + %1$d zpráv + %1$d zpráv Nová zpráva @@ -311,6 +332,13 @@ Máte %1$d nových zpráv Máte %1$d nových zpráv + + %1$d vybraná + %1$d vybrané + %1$d vybraných + %1$d vybraných + + Zprávy odstraněné Žádné informace o poznámkách Body @@ -372,8 +400,8 @@ Žádné informace o domácích úkolech - Označit jako hotové - Neudělané + Vykonané + Nevykonané Přidat domácí úkol Domácí úkol byl úspěšně přidán Domácí úkol byl úspěšně odstraněn @@ -636,6 +664,7 @@ Zkopírováno Vrátit Změnit + Přidat do kalendáře Žádné lekce Vybrat motiv @@ -657,21 +686,23 @@ Známky barevné schéma Třídění předmětů Jazyk - Upozornění + Oznámení 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. - Zobrazit upozornění o ladění + Zobrazit oznámení + Zobrazit oznámení o nadcházející lekci + Nastavit oznámení o nadcházející lekci jako trvalé + Vypnout, když oznámení není ve vašem hodinkách/náramku viditelné + Otevřít systémová nastavení oznámení + Opravte problémy se synchronizací a oznámením + Vaše zařízení může mít problémy se synchronizací dat as oznámeními.\n\nChcete-li je opravit, přidejte Wulkanového do funkce Autostart a vypněte optimalizaci/úsporu baterie v nastavení systému telefonu. + Zobrazit oznámení 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 - Upozornění o nadcházející lekci + Oznámení oficiální aplikace + Zachytit oznámení oficiální aplikací + Odstranit oznámení oficiální aplikace po zachycení + Zachytit oznámení + S touto funkcí můžete získat náhradu push oznámení jako v oficiální aplikaci. Vše, co musíte udělat, je povolit Wulkanowému číst všechna vaše oznámení 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í oznámení.\n\nPOUZE PRO POKROČILÉ UŽIVATELE + Oznámení o nadcházející lekci Musíte povolit Wulkanovému nastavit budíky a připomenutí v nastavení vašeho systému pro použití této funkce. Přejít do nastavení Synchronizace @@ -689,6 +720,10 @@ Odpovědět s historií zpráv Vypočítat aritmetický průměr, pokud žádná známka nemá váhu Podpora + Ochrana osobních údajů + Souhlasy + Souhlas se zpracováním údajů souvisejících s reklamami + Zobrazit reklamy v aplikaci 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ů @@ -696,9 +731,16 @@ Ochrana osobních údajů Reklama se načítá Děkujeme za vaši podporu, vraťte se později pro více reklam + Můžeme použít Vaše data k zobrazení reklam? + Volbu můžete kdykoliv změnit v nastavení aplikace. Můžeme použít Vaše data k zobrazení reklam šitých pro vás nebo pomocí méně vašich dat zobrazovat nepřizpůsobené reklamy. Podrobnosti naleznete v našich Zásadách ochrany osobních údajů + Přizpůsobené reklamy + Nepřizpůsobené reklamy + Mám ukončené 18 let + Ano, přizpůsobené reklamy + Ano, nepřizpůsobené reklamy Pokročilé Vzhled a chování - Upozornění + Oznámení Synchronizace Reklamy Známky @@ -711,8 +753,8 @@ Zprávy Vzhled a chování Jazyky, motivy, třídění předmětů - Upozornění aplikace, oprava problémů - Upozornění + Oznámení aplikací, oprava problémů + Oznámení Synchronizace Automatická aktualizace, interval aktualizací Hodnota plusu a mínusu, výpočet průměru @@ -728,7 +770,7 @@ Nové zprávy Nové poznámky Nové školní oznámení - Push upozornění + Push oznámení Nadcházející lekce Ladění Změny plánu lekcí @@ -747,11 +789,12 @@ Aktualizace selhala! Wulkanowy nemusí fungovat správně. Zvažte aktualizaci Žádné internetové připojení + Vyskytla se chyba. Zkontrolujte hodiny svého zařízení Nelze se připojit ke deníku. Servery mohou být přetíženy. Prosím zkuste to znovu později Načítání dat se nezdařilo. Prosím zkuste to znovu později Je vyžadována změna hesla pro deník - Probíhá údržba UONET+ deník. Zkuste to později znovu - Neznámá chyba denika UONET+. Prosím zkuste to znovu později + Probíhá údržba deníku UONET+. Zkuste to později znovu + Neznámá chyba deniku UONET+. Prosím zkuste to znovu později Neznámá chyba aplikace. Prosím zkuste to znovu později Vyskytla se neočekávaná chyba Funkce je deaktivována přes vaší školou diff --git a/app/src/main/res/values-de/preferences_values.xml b/app/src/main/res/values-de/preferences_values.xml index 23e54cb78..d9cac1959 100644 --- a/app/src/main/res/values-de/preferences_values.xml +++ b/app/src/main/res/values-de/preferences_values.xml @@ -34,6 +34,7 @@ Alphabetisch Nach Datum + Nach Durchschnitt Dzienniczek+ @@ -41,9 +42,9 @@ Farben der Bewertungen im Logbuch - Up to 1 at once - Always expanded - Unlimited expansions + Bis zu 1 auf einmal + Immer erweitert + Unbegrenzte Erweiterungen Durchschnitt der Noten aus beiden Semestern diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index f81724a21..4dfeee4f6 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 - Stundenplan + Zeitplan Einstellungen Mehr Über die Applikation @@ -17,7 +17,7 @@ Lizenzen Nachrichten neue Nachricht - New homework + Neue Hausaufgaben Eintragen und Erfolgen Hausaufgaben Konten-Manager @@ -55,8 +55,8 @@ Ungültige symbol Schüler nicht gefunden. Überprüfen Sie das Symbol und die gewählte Variation des UONET+ Registers 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. + 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 + Wählen Sie die Studenten aus, die sich bei der Anwendung anmelden sollen Andere Optionen In diesem Modus funktioniert eine Glücknummer, eine Klassenstatistik, eine Zusammenfassung der Anwesenheit, eine Entschuldigung für die Abwesenheit, abgeschlossene Lektionen, Schulinformationen und eine Vorschau der Liste der registrierten Geräte nicht In diesem Modus werden dieselben Daten angezeigt, die auf der Klassenbuch-Website angezeigt werden @@ -71,11 +71,15 @@ Ihr Konto wiederherstellen Wiederherstellen Student ist bereits angemeldet + Standard Kundenbetreuer Anmelden Die Sitzung ist abgelaufen Die Sitzung ist abgelaufen, bitte loggen Sie sich erneut ein + Anwendungsunterstützung + Gefällt Ihnen diese App? Unterstützen Sie ihre Entwicklung, indem Sie nicht-invasive Werbung aktivieren, die Sie jederzeit deaktivieren können + Werbung aktivieren Note Semester %d @@ -93,9 +97,9 @@ 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 + Der berechnete Mittelwert ist das arithmetische Mittel, das aus den Durchschnittswerten der Probanden errechnet wird. Es erlaubt Ihnen, den ungefähre endgültigen Durchschnitt zu kennen. Sie wird auf eine vom Anwender in den Anwendungseinstellungen gewählte Weise berechnet. Es wird empfohlen, die entsprechende Option zu wählen. Das liegt daran, dass die Berechnung der Schuldurchschnitte unterschiedlich ist. Wenn Ihre Schule den Durchschnitt der Fächer auf der Vulcan-Seite angibt, lädt die Anwendung diese Fächer herunter und berechnet nicht den Durchschnitt. Dies kann geändert werden, indem die Berechnung des Durchschnitts in den Anwendungseinstellungen erzwungen wird. \n\nDurchschnitt der Noten nur aus dem ausgewählten Semester :\n1. Berechnung des gewichteten Durchschnitts für jedes Fach in einem bestimmten Semester\n2. Addition der berechneten Durchschnittswerte\n3. Berechnung des arithmetischen Mittels der summierten Durchschnitte\nDurchschnitt der Durchschnitte aus beiden Semestern:\n1. Berechnung des gewichteten Durchschnitts für jedes Fach in Semester 1 und 2\n2. Berechnung des arithmetischen Mittels der berechneten Durchschnitte für Semester 1 und 2 für jedes Fach. \n3. Hinzufügen von berechneten Durchschnittswerten\n4. Berechnung des arithmetischen Mittels der summierten Durchschnitte\nDurchschnitt der Noten aus dem ganzen Jahr:\n1. Berechnung des gewichteten Jahresdurchschnitts für jedes Fach. Der Abschlussdurchschnitt im 1. Semester ist irrelevant. \n2. Addition der berechneten Durchschnittswerte\n3. Berechnung des arithmetischen Mittels der summierten Mittelwerte 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 + Der Final Average ist das arithmetische Mittel, das aus allen derzeit verfügbaren Abschlussnoten des jeweiligen Semesters berechnet wird. \n\nDas Berechnungsschema besteht aus folgenden Schritten:\n1. Zusammenfassung der von den Lehrern gegebenen Abschlussnoten\n2. Division durch die Anzahl der Fächer, die bereits bewertet wurden Finaler Durchschnitt aus %1$d von %2$d Schulfächern Zusammenfassung @@ -105,7 +109,9 @@ Semester Punkte Legende - Durchschnitt: %1$s + Klassendurchschnitt: %1$s + Dein Durchschnitt: %1$s + Deine Note: %1$s Klasse Schüler @@ -151,25 +157,25 @@ 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 + %1$s Lektion %2$d - %3$s + Änderung des Raumes von %1$s zu %2$s + Wechsel des Lehrers von %1$s zu %2$s + Thema von %1$s zu %2$s wechseln - Timetable change - Timetable changes + Änderung des Zeitplans + Änderungen des Zeitplans - %1$s - %2$d change in timetable - %1$s - %2$d changes in timetable + %1$s – %2$d Änderung im Zeitplan + %1$s - %2$d Änderungen im Zeitplan - %1$d change in timetable - %1$d changes in timetable + %1$d Änderung im Zeitplan + %1$d Änderungen im Zeitplan - %d change - %d changes + %d Änderung + %d Änderungen Beendete Lektionen @@ -182,6 +188,17 @@ Zusätzliche Lektionen Zusätzliche Lektionen anzeigen Keine Informationen über zusätzlichen Lektionen + Neue Lektion + Neue zusätzliche Lektion + Zusätzliche Lektion erfolgreich hinzugefügt + Zusätzliche Lektion erfolgreich gelöscht + Wöchentlich wiederholen + Zusätzliche Lektion löschen + Nur diese Lektion + Alle in der Reihe + Startzeit + Endzeit + Endzeit muss grösser sein als Startzeit Übersicht über die Schulbesuch Aus schulischen Gründen abwesend @@ -201,16 +218,16 @@ Sie müssen mindestens eine Abwesenheit auswählen! Verzeihung - New attendance - New attendance + Neue Teilnehmerzahl + Neue Teilnehmerzahl - %1$d new attendance - %1$d attendance + %1$d neue Teilnahme + %1$d Teilnahme - %d attendance - %d attendance + %d Teilnahme + %d Teilnahme Gesamt @@ -223,8 +240,8 @@ Neue prüfungen - Du hast %d neue Prüfung - Du hast %d neue Prüfungen + %d neue Prüfung + %d neue Prüfungen %d prüfung @@ -238,13 +255,18 @@ Keine Nachrichten Von: An: - Datum: %s + Datum: %1$s Antwort Weiterleiten - Löschen - In den Korb wandern + Alle auswählen + Alle abwählen + In Papierkorb verschieben Dauerhaft löschen Nachricht erfolgreich gelöscht + schüler + Eltern + Betreuer + Mitarbeiter Teilen Drucken Thema @@ -256,10 +278,9 @@ Nur ungelesen Nur mit Anhängen Lesen: %s - Lesen von: %1$d von %2$d Personen - %d nachricht - %d nachrichten + %1$d Nachricht + %1$d Nachrichten Neu nachricht @@ -271,6 +292,11 @@ Du hast %1$d nachricht bekommen Du hast %1$d nachrichten bekommen + + %1$d ausgewählt + %1$d ausgewählt + + Nachrichten gelöscht Keine Informationen über Eintragen Punkte @@ -316,9 +342,9 @@ Keine Informationen über Hausaufgaben Gemacht Unvollständig - Add homework - Homework added successfully - Homework deleted successfully + Hausaufgaben hinzufügen + Hausaufgaben erfolgreich hinzugefügt + Heimarbeit erfolgreich gelöscht Anhänge Neue hausaufgaben @@ -472,7 +498,7 @@ Lektionen (Morgen) - (Today and tomorrow) + (Heute und morgen) Gleich: Bald: Erstens: @@ -546,10 +572,11 @@ Nein Speichern Titel - Add + Hinzufügen Kopiert lösen Ändern + Zum Kalender hinzufügen Keine Lektionen Thema wählen @@ -563,7 +590,7 @@ Mittelwertberechnung durch App erzwingen Anwesendheit zeigen Thema - Grades expanding + Steigende Sorten Aktuelle Lektion markieren Gruppen neben Schulfächen anzeigen Liste der Diagramme in Klassenbewertungen anzeigen @@ -572,7 +599,7 @@ Schulfachen sortieren Sprache Benachrichtigungen - Other + Sonstiges Benachrichtigungen anzeigen Benachrichtigungen über bevorstehende Lektionen anzeigen Festlegen einer Benachrichtigung über die bevorstehende Lektion dauerhaft @@ -582,12 +609,14 @@ 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. Debug-Benachrichtigungen anzeigen Synchronisierung ist deaktiviert + Offizielle Benachrichtigungen Offizielle App-Benachrichtigungen erfassen + Entfernen Sie offizielle App-Benachrichtigungen nach der Erfassung 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 - Upcoming lesson notifications - You must allow the Wulkanowy app to set alarms and reminders in your system settings to use this feature. - Go to settings + Mit dieser Funktion können Sie einen Ersatz für Push-Benachrichtigungen erhalten, wie in der offiziellen App. Alles, was Sie tun müssen, ist es Wulkanowy erlauben, alle Benachrichtigungen in Ihren Systemeinstellungen zu erhalten.\n\nWie funktioniert es?\nWenn Sie eine Benachrichtigung in Dziennik VULCAN erhalten, Wulkanowy wird benachrichtigt (dafür sind diese zusätzlichen Berechtigungen) und wird eine Synchronisierung auslösen, so dass eine eigene Benachrichtigung gesendet werden kann.\n\nNUR FÜR FORTGESCHRITTENE BENUTZER + Bevorstehende Unterrichtsbenachrichtigungen + Sie müssen der Wulkanowy-App erlauben, in Ihren Systemeinstellungen Alarme und Erinnerungen einzustellen, damit diese Funktion verwendet werden kann. + Gehe zu den Einstellungen Synchronisierung Automatische Aktualisierung An Feiertagen suspendiert @@ -602,26 +631,37 @@ 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 - Thank you for your support, come back later for more ads + Unterstützung + Privacy Policy + Agreements + Consent to processing of data related to ads + Show ads in app + Einzelanzeige ansehen, um Projekt zu unterstützen + Einwilligung in die Datenverarbeitung + Um eine Anzeige zu sehen, müssen Sie mit den Datenverarbeitungsbedingungen unserer Datenschutzerklärung einverstanden sein + Einverstanden + Datenschutzerklärung + Anzeige wird geladen + Vielen Dank für Ihre Unterstützung, kommen Sie später wieder für weitere Anzeigen + Können wir Ihre Daten zur Anzeige von Werbung verwenden? + Sie können Ihre Wahl jederzeit in den App-Einstellungen ändern. Wir verwenden Ihre Daten, um auf Sie zugeschnittene Anzeigen anzuzeigen oder unter Verwendung weniger Ihrer Daten nicht personalisierte Werbung anzuzeigen. Bitte lesen Sie unsere Datenschutzerklärung für Details + Personalisierte Werbung + keine personalisierte Werbung + I am over 18 years old + Yes, personalized ads + Yes, non-personalized ads Erweitert Aussehen & Verhalten Benachrichtigungen Synchronisierung - Advertisements + Werbung Noten Dashboard Sichtbarkeit der Kacheln Schulbesuch Stundenplan Noten - Calculated average + Berechneter Durchschnitt Nachrichten Aussehen & Verhalten Sprachen, Themen, Schulfachen sortieren @@ -631,8 +671,8 @@ Automatisches Update, Synchronisierungsintervall Plus und Minus Werte, Durchschnittsberechnung Erweitert - App version, contributors, social portals - Displaying advertisements, project support + App-Version, Mitwirkende, soziale Portale + Anzeigen, Projektunterstützung Neue Noten Neue Hausaufgaben @@ -645,8 +685,8 @@ Push-Benachrichtigungen Bevorstehende Lektionen Debuggen - Timetable change - New attendance + Änderung des Zeitplans + Neue Teilnehmerzahl Schwarz Rot @@ -661,6 +701,7 @@ Update fehlgeschlagen! Wulkanowy funktioniert möglicherweise nicht richtig. Überlegen Sie die Aktualisierung Keine Internetverbindung + Es ist ein Fehler aufgetreten. Überprüfen Sie Ihre Geräteuhr Registrierungsverbindung fehlgeschlagen. Server können überlastet sein. Bitte versuchen Sie es später noch einmal Das Laden der Daten ist fehlgeschlagen. Bitte versuchen Sie es später noch einmal Passwortänderung für Registrierung erforderlich @@ -670,5 +711,5 @@ Ein unerwarteter Fehler ist aufgetreten Funktion, die von Ihrer Schule deaktiviert wurde Feature in diesem Modus nicht verfügbar - This field is required + Dieses Feld ist erforderlich diff --git a/app/src/main/res/values-pl/preferences_values.xml b/app/src/main/res/values-pl/preferences_values.xml index c823e9608..456005747 100644 --- a/app/src/main/res/values-pl/preferences_values.xml +++ b/app/src/main/res/values-pl/preferences_values.xml @@ -34,6 +34,7 @@ Alfabetycznie Według daty + Według średniej Dzienniczek+ diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 93bdd5354..c9abbd022 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -55,7 +55,7 @@ Nieprawidłowy symbol Nie znaleziono ucznia. Sprawdź poprawność symbolu i wybranej odmiany dziennika UONET+ 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) + 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 Wybierz uczniów do zalogowania w aplikacji Inne opcje W tym trybie nie działa szczęśliwy numerek, uczeń na tle klasy, podsumowanie frekwencji, usprawiedliwianie nieobecności, lekcje zrealizowane, informacje o szkole i podgląd listy zarejestrowanych urządzeń @@ -71,11 +71,15 @@ Przywróć swoje konto Przywróć Uczeń jest już zalogowany + Standardowa Menadżer kont Zaloguj się Sesja wygasła Sesja wygasła, zaloguj się ponownie + Wparcie aplikacji + Podoba Ci się ta aplikacja? Wspieraj jej rozwój poprzez włączenie nieinwazyjnych reklam, które możesz wyłączyć w dowolnym momencie + Włącz reklamy Ocena Semestr %d @@ -93,7 +97,7 @@ 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 + Obliczona średnia jest średnią arytmetyczną obliczoną ze średnich przedmiotów. Pozwala ona na poznanie przybliżonej średniej końcowej. Jest obliczana w sposób wybrany przez użytkownika w ustawieniach aplikacji. Zaleca się wybranie odpowiedniej opcji. Dzieje się tak dlatego, że obliczanie średnich w szkołach różni się. Dodatkowo, jeśli twoja szkoła ma włączone średnie przedmiotów na stronie dziennika Vulcan, aplikacja pobiera je i ich nie oblicza. Można to zmienić, wymuszając obliczanie średniej w ustawieniach aplikacji.\n\nŚrednia ocen tylko z wybranego semestru:\n1. Obliczanie średniej arytmetycznej każdego przedmiotu w danym semestrze\n2. Zsumowanie obliczonych średnich\n3. Obliczanie średniej arytmetycznej zsumowanych średnich\n\nŚrednia ze średnich z obu semestrów:\n1.Obliczanie średniej arytmetycznej każdego przedmiotu w semestrze 1 i 2\n2. Obliczanie średniej arytmetycznej obliczonych średnich w semestrze 1 i 2 każdego przedmiotu.\n3. Zsumowanie obliczonych średnich\n4. Obliczanie średniej arytmetycznej zsumowanych średnich\n\nŚrednia wszystkich ocen z całego roku:\n1. Obliczanie średniej arytmetycznej z każdego przedmiotu w ciągu całego roku. Końcowa ocena w 1 semestrze jest bez znaczenia.\n2. Zsumowanie obliczonych średnich\n3. Obliczanie średniej arytmetycznej z zsumowanych średnich 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 @@ -105,7 +109,9 @@ Semestralne Punkty Legenda - Średnia: %1$s + Średnia klasy: %1$s + Twoja średnia: %1$s + Twoja ocena: %1$s Klasa Uczeń @@ -204,6 +210,17 @@ Dodatkowe lekcje Pokaż dodatkowe lekcje Brak informacji o dodatkowych lekcjach + Nowa lekcja + Nowa dodatkowa lekcja + Dodatkowa lekcja dodana pomyślnie + Dodatkowa lekcja usunięta pomyślnie + Powtarzaj co tydzień + Usuń dodatkową lekcję + Tylko ta lekcja + Wszystkie w serii + Godzina rozpoczęcia + Godzina zakończenia + Godzina zakończenia musi być późniejsza niż godzina rozpoczęcia Podsumowanie frekwencji Nieobecność z przyczyn szkolnych @@ -253,10 +270,10 @@ Nowe sprawdziany - Masz %d nowy sprawdzian - Masz %d nowe sprawdziany - Masz %d nowych sprawdzianów - Masz %d nowych sprawdzianów + %d nowy sprawdzian + %d nowe sprawdziany + %d nowych sprawdzianów + %d nowych sprawdzianów %d sprawdzian @@ -272,13 +289,18 @@ Brak wiadomości Od: Do: - Data: %s + Data: %1$s Odpowiedz Prześlij dalej - Usuń - Przenieś do kosza + Zaznacz wszystkie + Odznacz wszystkie + Przenieś do kosza Usuń trwale Wiadomość usunięta pomyślnie + uczeń + rodzic + opiekun + pracownik Udostępnij Drukuj Temat @@ -290,12 +312,11 @@ Tylko nieprzeczytane Tylko z załącznikami Przeczytana: %s - Przeczytana przez: %1$d z %2$d osób - %d wiadomość - %d wiadomości - %d wiadomości - %d wiadomości + %1$d wiadomość + %1$d wiadomości + %1$d wiadomości + %1$d wiadomości Nowa wiadomość @@ -311,6 +332,13 @@ Masz %1$d nowych wiadomości Masz %1$d nowych wiadomości + + %1$d wybrana + %1$d wybrane + %1$d wybranych + %1$d wybranych + + Wiadomości zostały usunięte Brak informacji o uwagach Punkty @@ -636,6 +664,7 @@ Skopiowano Cofnij Zmień + Dodaj do kalendarza Brak lekcji Wybierz motyw @@ -668,7 +697,9 @@ 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. Pokazuj powiadomienia debugowania Synchronizacja jest wyłączona + Powiadomienia oficjalnej aplikacji Przechwytywanie powiadomień oficjalnej aplikacji + Usuwaj powiadomienia oficjalnej aplikacji po przechwyceniu 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 Powiadomienia o nadchodzących lekcjach @@ -676,7 +707,7 @@ Przejdź do ustawień Synchronizacja Automatyczna aktualizacja - Zawieszona na wakacjach + Wstrzymana podczas wakacji Interwał aktualizacji Tylko WiFi Synchronizuj teraz @@ -689,6 +720,10 @@ Odpowiadaj z historią wiadomości Licz średnią arytmetyczną, gdy żadna ocena nie ma wagi Wsparcie + Polityka prywatności + Zgody + Zgoda na przetwarzanie danych związanych z reklamami + Pokazuj reklamy w aplikacji Obejrzyj pojedynczą reklamę, aby wesprzeć projekt Zgoda na przetwarzanie danych Aby obejrzeć reklamę, musisz zaakceptować warunki przetwarzania danych zawarte w naszej Polityce Prywatności @@ -696,6 +731,13 @@ Polityka prywatności Ładowanie reklamy Dziękujemy za wsparcie, wróć później po więcej reklam + Czy możemy używać Twoich danych do wyświetlania reklam? + Możesz zmienić swój wybór w dowolnym momencie w ustawieniach aplikacji. Możemy wykorzystać Twoje dane do wyświetlania reklam dostosowanych do Ciebie lub, przy użyciu mniejszej ilości danych, wyświetlić niepersonalizowane reklamy. Zobacz naszą Politykę Prywatności, aby uzyskać więcej informacji + Spersonalizowane reklamy + Niespersonalizowane reklamy + Mam ukończone 18 lat + Tak, spersonalizowane reklamy + Tak, niespersonalizowane reklamy Zaawansowane Wygląd i zachowanie Powiadomienia @@ -747,6 +789,7 @@ Aktualizacja nie powiodła się! Wulkanowy może nie działać prawidłowo. Rozważ aktualizację Brak połączenia z internetem + Wystąpił błąd. Sprawdź poprawność daty w urządzeniu Nie udało się połączyć z dziennikiem. Serwery mogą być przeciążone. Spróbuj ponownie później Ładowanie danych nie powiodło się. Spróbuj ponownie później Wymagana zmiana hasła do dziennika diff --git a/app/src/main/res/values-ru/preferences_values.xml b/app/src/main/res/values-ru/preferences_values.xml index cfdaa9577..8a8c260da 100644 --- a/app/src/main/res/values-ru/preferences_values.xml +++ b/app/src/main/res/values-ru/preferences_values.xml @@ -32,8 +32,9 @@ 0,75 - Алфавитный + В алфавитном порядке По дате + По средней Dzienniczek+ @@ -43,22 +44,22 @@ До 1 за раз Всегда развернуто - Неограниченные расширения + Неограниченно - Средние оценки только с выбранного семестра - Средние значения для обоих семестров - Средняя оценок со всего года + Средняя из оценок выбранного семестра + Средняя из средних оценок семестров + Средняя из оценок со всего года Счастливый номер - Непрочитанные сообщения + Непрочитанные письма Посещаемость Уроки Оценки Домашняя работа - Объявления школ - Экзамены - Конференции + Объявления школы + Тесты + Встречи diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 33ec21826..e1abb0293 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -12,92 +12,96 @@ О приложении Просмотр журнала Отладка - Отладка уведомления + Отладка уведомлений Разработчики Лицензии - Сообщения - Новое сообщение - Новая домашняя работа - Предупреждения и свершения + Письма + Написать + Новое домашнее задание + Замечания и свершения Домашние задания - Менеджер аккаунтов - Выбор учетной записи + Управлять аккаунтами + Выбрать аккаунт Данные аккаунта - Информация о студенте - Панель + Информация о ученике + Главная Центр уведомлений %1$d семестр, %2$d/%3$d - Авторизируйтесь при помощи аккаунта ученика или родителя - Введите символ со страницы регистрации: <b>%1$s</b> - Имя пользователя - Электронная почта + Воспользуйтесь аккаунтом ученика/родителя + Введите symbol аккаунта со страницы регистрации: <b>%1$s</b> + Логин + E-mail Логин, PESEL или электронная почта Пароль - UONET + вариант регистрации + Тип дневника UONET+ Мобильный API Scraper - Гибрид + Hybrid Token PIN Symbol Войти - Слишком короткий пароль + Пароль слишком короткий Данные для входа указаны неверно - %1$s. Убедитесь, что ниже выбран правильный UONET+ вариант регистра - Неправильный PIN + %1$s. Убедитесь, что ниже выбран правильный тип дневника UONET+ + Неверный PIN Неверный token Token просрочен - Неверный адрес электронной почты - Используйте назначенный логин вместо электронной почты - Использовать назначенный логин или email в @%1$s - Неправильный символ - Студент не найден. Подтвердите символ и выбранный вариант регистра UONET+ + Неверный e-mail + Используйте назначенный логин вместо e-mail + Используйте назначенный логин или email в @%1$s + Неверный symbol + Ученик не найден. Проверьте symbol и выбранный тип дненика UONET+ Данный ученик уже авторизован - Этот символ можно найти на странице регистрации в  Ученик →  Телефонный доступ →   Зарегистрируйте мобильное устройство.\n\nУбедитесь, что вы установили соответствующий вариант регистра в поле Разновидностью бревна UONET+ на предыдущем экране. Вулкановый на данный момент не обнаруживает дошкольников + Symbol можно найти на странице регистрации в  Uczeń → Dostęp Mobilny → Zarejestruj urządzenie mobilne.\n\nУбедитесь, что вы выбрали соответствующий тип дневника в поле Тип дневника UONET+ на предыдущем экране Выберите учеников для авторизации в приложении Другие варианты В этом режиме не работают: счастливый номер, статистика класса по оценкам, статистика посещаемости и уроков, информация о школе и список зарегистрированных устройств Scraper - режим, который показывает данные так же, как и сайт дневника Hybrid - это комбинация лучших функций остальных двух режимов. Он работает быстрее, чем Scraper, и вводит функции, которых нет в режиме Mobile API. Находится в экспериментальной стадии - Политика приватности + Политика конфиденциальности Проблемы с авторизацией? Свяжитесь с нами! - Электронная почта + E-mail Discord Отправить письмо - Убедитесь, что вы выбрали правильный вариант регистра UONET+! - Я забыл свой пароль + Убедитесь, что вы выбрали правильный тип дневника UONET+ + Забыли пароль? Восстановите свой аккаунт Восстановить - Студент уже вошел в систему + Ученик уже авторизован + Стандартный Менеджер аккаунтов Войти Сеанс истёк - Сеанс истёк, пожалуйста, войдите ещё раз + Сеанс истёк, авторизуйтесь снова + Application support + Do you like this app? Support its development by enabling non-invasive ads that you can disable at any time + Enable ads Оценка - Семестр %d + %d семестр Сменить семестр - Оценки отсутствуют + Нет оценок Стоимость Стоимость: %s Комментарий Количество новых оценок: %1$d Средняя оценка: %1$.2f Баллы: %s - Средняя оценка отсутствует + Нет средней оценки Сумма баллов Итоговая оценка Ожидаемая оценка Рассчитанная средняя оценка - Как рассчитывается средняя работа? - Расчетное среднее - это среднее арифметическое, рассчитанное на основе средних значений испытуемых. Это позволяет узнать приблизительное итоговое среднее значение. Он рассчитывается способом, выбранным пользователем в настройках приложения. Рекомендуется выбрать подходящий вариант. Это потому, что расчет средних показателей школы отличается. Кроме того, если ваша школа сообщает среднее значение по предметам на странице 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. Деление по количеству уже оцененных предметов + Как работает \"Рассчитанная средняя оценка\"? + The Calculated Average is the arithmetic average calculated from the subjects averages. It allows you to know the approximate final average. It is calculated in a way selected by the user in the application settings. It is recommended that you choose the appropriate option. This is because the calculation of school averages differs. Additionally, if your school reports the average of the subjects on the Vulcan page, the application downloads them and does not calculate these averages. This can be changed by forcing the calculation of the average in the application settings.\n\nAverage of grades only from selected semester:\n1. Calculating the weighted average for each subject in a given semester\n2.Adding calculated averages\n3. Calculation of the arithmetic average of the summed averages\n\nAverage of averages from both semesters:\n1.Calculating the weighted average for each subject in semester 1 and 2\n2. Calculating the arithmetic average of the calculated averages for semesters 1 and 2 for each subject.\n3. Adding calculated averages\n4. Calculation of the arithmetic average of the summed averages\n\nAverage of grades from the whole year:\n1. Calculating weighted average over the year for each subject. The final average in the 1st semester is irrelevant.\n2. Adding calculated averages\n3. Calculating the arithmetic average of summed averages + Как работает \"Итоговая средняя оценка\"? + Итоговая средняя оценка - это среднее арифметическое, рассчитанное из всех имеющихся на данный момент итоговых оценок в семестре.\n\nРассчет происходит следующим образом:\n1. Суммирование итоговых оценок, выставленных преподавателями\n2. Полученная сумма делится на число предметов, по которым выставлены оценки Итоговая средняя оценка - от %1$d из %2$d субъектов + %1$d из %2$d предметов Итоги Класс Пометить как \"прочитанное\" @@ -105,9 +109,11 @@ За семестр Баллы Легенда - Средняя: %1$s + Средняя класса: %1$s + Ваша средняя: %1$s + Ваша оценка: %1$s Класс - Студент + Ученик %d оценка %d оценки @@ -133,22 +139,22 @@ Новые итоговые оценки - Вы получили %1$d оценку - Вы получили %1$d оценки - Вы получили %1$d оценок - Вы получили %1$d оценок + Вы получили %1$d новую оценку + Вы получили %1$d новые оценки + Вы получили %1$d новых оценок + Вы получили %1$d новых оценок - Вы получили %1$d ожидаемую оценку - Вы получили %1$d ожидаемые оценки - Вы получили %1$d ожидаемых оценок - Вы получили %1$d ожидаемых оценок + Вы получили %1$d новую ожидаемую оценку + Вы получили %1$d новые ожидаемые оценки + Вы получили %1$d новых ожидаемых оценок + Вы получили %1$d новых ожидаемых оценок - Вы получили %1$d итоговою оценку - Вы получили %1$d итоговых оценки - Вы получили %1$d итоговых оценок - Вы получили %1$d финальные оценки + Вы получили %1$d новую итоговую оценку + Вы получили %1$d новых итоговых оценки + Вы получили %1$d новых итоговых оценок + Вы получили %1$d новых итоговые оценки Урок @@ -166,36 +172,36 @@ Следующий: %s Позже: %s %1$s урок %2$d - %3$s - Изменить комнату с %1$s на %2$s - Изменить учителя с %1$s на %2$s - Изменить тему с %1$s на %2$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$s - %2$d изменений в расписании - %1$s - %2$d изменений в расписании + %1$s - %2$d изменений в расписании - %1$d - изменений в расписании - %1$d изменение в расписании - %1$d изменение в расписании + %1$d изменение в расписании + %1$d изменения в расписании + %1$d изменений в расписании %1$d изменений в расписании %d изменение - %d изменение - %d изменение + %d изменения + %d изменений %d изменений Проведённые уроки - Просмотреть проведённые уроки + Показать проведённые уроки Нет информации о проведённых уроках Тема Отсутствие @@ -204,41 +210,52 @@ Дополнительные уроки Показать дополнительные уроки Нет информации о дополнительных уроках + Новый урок + Новый дополнительный урок + Дополнительный урок успешно добавлен + Дополнительный урок успешно удален + Повторять еженедельно + Удалить дополнительный урок + Только этот урок + Все в серии + Время начала + Время окончания + Время окончания должно быть больше, чем время начала Итоговая посещаемость Отсутствие по школьным причинам Отсутствие по уважительной причине Отсутствие по неуважительной причине Освобождение - Опоздание по уважительным причинам - Опоздание по неуважительным причинам + Опоздание по уважительной причине + Опоздание по неуважительной причине Присутствие Удалено Неизвестно Урок № - Данные не найдены + Нет записей Причина отсутствия (необязательно) - Послать - Запрос на освобождение оправдания успешно отправлен! - Выберите хотя-бы одно отсутствие - Изменить статус + Отправить + Причина отсутствия успешно отправлена. + Выберите хотя бы одно отсутствие. + Указать причину - Новое посещение - Новое посещение - Новое посещение - Новое посещение + Новая запись о посещаемости + Новые записи о посещаемости + Новые записи о посещаемости + Новые записи о посещаемости - %1$d новое посещения - %1$d новое посещение - %1$d новое посещение - %1$d новое посещения + %1$d новая запись о посещаемости + %1$d новые записи о посещаемости + %1$d новых записей о посещаемости + %1$d новых записей о посещаемости - %d посещаемость - %d посещаемость - %d посещаемость - %d посещаемость + %d запись о посещаемости + %d записи о посещаемости + %d записей о посещаемости + %d записей о посещаемости Общая @@ -247,55 +264,59 @@ Тип Дата записи - Новый экзамен - Новый экзамен - Новый экзамен - Новые экзамены + Новый тест + Новые тесты + Новые тесты + Новые тесты - Вы получили %d новый экзамен - Вы получили %d новый экзамен - Вы получили %d новый экзамен - Вы получили %d новых экзаменов + %d новый тест + %d новых теста + %d новых тестов + %d новых тестов - %d экзамен - %d экзамен - %d экзамен - %d экзаменов + %d тест + %d теста + %d тестов + %d тестов Полученные Отправленные Корзина (нет темы) - Нет сообщений + Нет писем От: Кому: - Дата: %s + Дата: %1$s Ответ Переслать - Удалить - Перенести в корзину + Выбрать все + Отменить выбор + Перенести в корзину Удалить навсегда - Сообщение успешно удалено + Письмо успешно удалено + student + parent + guardian + employee Поделиться Печать Тема - Текст - Сообщение успешно отправлено - Сообщения не существует + Содержание + Письмо успешно отправлено + Письма не существует Вы должны выбрать как минимум одного получателя Текст сообщения должен содержать как минимум 3 знака Только непрочитанные Только с вложениями - Чтение: %s - Прочитано: %1$d из %2$d человек + Прочитано: %s - %d сообщение - %d сообщения - %d сообщений - %d сообщений + %1$d сообщение + %1$d сообщения + %1$d сообщений + %1$d сообщений Новое сообщение @@ -303,16 +324,23 @@ Новые сообщения Новые сообщения - Вы хотите восстановить черновичное сообщение? - Вы хотите восстановить черновик сообщения с получателями: %s? + Вы хотите восстановить черновик письма? + Вы хотите восстановить черновик письма с получателями: %s? Вы получили %1$d новое сообщение Вы получили %1$d новых сообщения Вы получили %1$d новых сообщений Вы получили %1$d новых сообщений + + %1$d выбрано + %1$d выбрано + %1$d выбрано + %1$d выбрано + + Сообщение удалено - Нет информации о заметках + Нет записей о замечаниях и свершениях Баллы %d предупреждение @@ -323,33 +351,33 @@ Новое предупреждение Новые предупреждения - Новых предупреждений - Новых предупреждений + Новые предупреждения + Новые предупреждения - Вы получили %1$d предупреждение - Вы получили %1$d предупреждения - Вы получили %1$d предупреждений - Вы получили %1$d предупреждений + Вы получили %1$d новое предупреждение + Вы получили %1$d новые предупреждения + Вы получили %1$d новых предупреждений + Вы получили %1$d новых предупреждений %d похвала - %d похвала - %d похвала - %d похвала + %d похвалы + %d похвал + %d похвал Новая похвала Новые похвалы - Новые свершения + Новые похвалы Новые похвалы - Вы получили %1$d похвалу - Вы получили %1$d похвалы - Вы получили %1$d похвалы - Вы получили %1$d похвалы + Вы получили %1$d новую похвалу + Вы получили %1$d новые похвалы + Вы получили %1$d новых похвал + Вы получили %1$d новых похвал @@ -365,34 +393,34 @@ Новые нейтральные замечания - Вы получили %1$d нейтральное замечание - Вы получили %1$d нейтральных замечания - Вы получили %1$d нейтральных замечаний - Вы получили %1$d нейтральных замечаний + Вы получили %1$d новое нейтральное замечание + Вы получили %1$d новые нейтральных замечания + Вы получили %1$d новых нейтральных замечаний + Вы получили %1$d новых нейтральных замечаний Нет домашних заданий - Отметить как выполненное - Отметить как невыполненное - Добавить домашнюю работу - Домашняя работа успешно добавлена - Домашняя работа успешно удалена + Завершено + Не завершено + Добавить домашнее задание + Домашнее задание успешно добавлена + Домашнее задание успешно удалена Вложения - Новая домашняя работа - Новая домашняя работа - Новая домашняя работа - Новая домашняя работа + Новое домашнее задание + Новые домашние работы + Новые домашние задания + Новые домашние задания - Вы получили %d новых домашних заданий + Вы получили %d новое домашнее задание Вы получили %d новых домашних заданий Вы получили %d новых домашних заданий Вы получили %d новых домашних заданий - %d домашних заданий - %d домашних заданий + %d домашнее задание + %d домашних задания %d домашних заданий %d домашних заданий @@ -401,15 +429,15 @@ Сегодняшний счастливый номер это Нет данных о счастливом номере Сегодняшний счастливый номер - Сегодняшний номер удачи: %s + Сегодняшний счастливый номер: %s Показать историю - История удачных чисел - Нет информации о номерах удачи + История счастливых номеров + Нет информации о счастливых номерах Мобильные устройства Нет устройств - Отменить регистрацию + Удалить Устройство удалено QR-код Token @@ -435,45 +463,45 @@ Встречи Нет информации о встречах - %d конференция - %d конференция - %d конференция - %d конференций + %d встреча + %d встречи + %d встреч + %d встреч - Новая конференция - Новая конференция - Новая конференция - Новые конференции + Новая встреча + Новые встречи + Новые встречи + Новые встречи - У вас %1$d новая конференция - У вас %1$d новая конференция - У вас %1$d новая конференция - У вас %1$d новых конференций + У вас %1$d новая встреча + У вас %1$d новые встречи + У вас %1$d новых встреч + У вас %1$d новых встреч - Присутствует на конференции + Присутствует на встрече Повестка дня - Объявления школ - Нет объявлений о школе + Объявления школы + Нет школьных объявлений - Объявление о школе %d - Объявление о школе %d - Объявление о школе %d - Объявления школы %d + %d школьное объявление + %d школьных объявления + %d школьных объявлений + %d школьных объявлений - Объявление о новой школе - Объявление о новой школе - Объявление о новой школе - Объявления о новых школах + Новое школьное объявление + Новые школьные объявления + Новые школьные объявления + Новые школьные объявления - У вас %1$d объявление о новой школе - У вас %1$d объявление о новой школе - У тебя %1$d новых школьных объявлений - У тебя %1$d новых школьных объявлений + У вас %1$d новое школьное объявление + У вас %1$d новые школьных объявления + У вас %1$d новых школьных объявлений + У вас %1$d новых школьных объявлений Добавить аккаунт @@ -483,29 +511,29 @@ Профиль ученика Профиль родителя Изменить данные - Менеджер аккаунтов - Выберите Студента + Управлять аккаунтами + Выбрать ученика Семья - Контакт - Детали проживания - Персональные данные + Контакты + Места жительства + Личная информация Версия приложения Разработчики - Список разработчиков \"Вулкановый\" + Список разработчиков приложения Возникла ошибка? Сообщить о ошибке FAQ Часто задаваемые вопросы Сервер Discord Присоединиться к сообществу приложения - Facebook фан-страница - Странница в твиттере - Подпишись на нас в твиттере - Поставьте лайк на нашей странице в Facebook - Политика приватности + Страница в Facebook + Страница в Twitter + Следите за нами в Twitter + Лайкните нас в Facebook + Политика конфиденциальности Правила хранения личных данных - Параметры системы + Системные настройки Открыть системные настройки Домашняя страница Помочь в развитии приложения @@ -519,23 +547,23 @@ Нет информации об ученике или семье ученика Имя - Фамилия + Второе имя Пол Польское гражданство - Фамилия + Девичья фамилия Имена матери и отца - Телефон + Домашний телефон Сотовый телефон - Эл. почта + E-mail Адрес проживания - Адрес регистрации - Адрес переписки + Адрес прописки + Почтовый адрес Фамилия и имя Степень родства Адрес - Телефоны - Муж - Женская + Номера телефонов + Мужской + Женский Фамилия Опекун @@ -543,7 +571,7 @@ Добавить ник Выберите цвет аватара - Поделиться логами + Отправить логи Обновить Уроки @@ -557,20 +585,20 @@ Далее: Позднее: - Еще %1$d урок - Еще %1$d урок - Еще %1$d урок - Ещё %1$d уроков + ещё %1$d урок + ещё %1$d урока + ещё %1$d уроков + ещё %1$d уроков до %1$s Нет предстоящих занятий Произошла ошибка при загрузке уроков - Домашняя работа + Домашнее задание Нет домашних заданий Произошла ошибка при загрузке домашнего задания - Ещё %1$d домашних заданий - Ещё %1$d домашних заданий + Ещё %1$d домашнее задание + Ещё %1$d домашних задания Ещё %1$d домашних заданий Ещё %1$d домашних заданий @@ -578,32 +606,32 @@ Последние оценки Нет новых оценок Произошла ошибка при загрузке оценок - Объявления школ - Нет текущих объявлений + Школьные объявления + Нет объявлений Произошла ошибка при загрузке объявлений - Ещё %1$d объявление - Ещё %1$d объявление - Ещё %1$d объявление - Ещё %1$d объявлений + ещё %1$d объявление + ещё %1$d объявления + ещё %1$d объявлений + ещё %1$d объявлений - Экзамены - Нет предстоящих экзаменов - Произошла ошибка при загрузке экзамена + Тесты + Нет предстоящих тестов + Произошла ошибка при загрузке тестов - Еще %1$d экзамен - Еще %1$d экзамен - Еще %1$d экзамен - ещё %1$d экзаменов + ещё %1$d тест + ещё %1$d теста + ещё %1$d тестов + ещё %1$d тестов - Конференции - Нет предстоящих конференций - Произошла ошибка при загрузке конференций + Встречи + Нет предстоящих встреч + Произошла ошибка при загрузке встреч - Еще %1$d конференция - Еще %1$d конференция - Еще %1$d конференция - Еще %1$d конференций + еще %1$d встреча + еще %1$d встречи + еще %1$d встреч + еще %1$d встреч Произошла ошибка при загрузке данных Отсутствует @@ -636,6 +664,7 @@ Скопировано Отменить Изменить + Добавить в календарь Нет уроков Выбрать тему @@ -645,94 +674,107 @@ Приложение Окно по умолчанию - Рассчитанные средние параметры + Параметры расчёта средних оценок Принудительно высчитать среднюю оценку через приложение - Показать присутствие + Показывать присутствия Тема - Расширяется оценка + Разворачивание оценок Отметить текущий урок Показать группы рядом с темами Показывать диаграммы в оценках класса Показать предметы без оценок - Схема цветов оценок + Цветовая схема оценок Сортировка уроков Язык Уведомления Прочее Показывать уведомления - Показывать уведомления о будущих уроках - Сделать уведомления о предстоящем уроке постоянным - Выключить, когда уведомление не отображается в чата/полосе - Открыть настройки уведомлений системы + Показывать уведомления о предстоящих уроках + Сделать уведомление о предстоящем уроке постоянным + Выключите, если уведомление не отображается на умных часах + Открыть системные настройки уведомлений Исправить проблемы с синхронизацией и уведомлениями На вашем устройстве могут быть проблемы с синхронизацией данных и уведомлениями.\n\nЧтобы их исправить, вам необходимо добавить Wulkanowy в авто-старт и выключить оптимизацию/экономию батареи в настройках устройства. - Показывать дебаг-уведомления + Показывать отладочные уведомления Синхронизация отключена - Записывать официальные уведомления - Показывать push-уведомления - С помощью этой функции вы можете получить замену push-уведомлений, как в официальном приложении. Все, что вам нужно сделать, это разрешить Wulkanowy получать все уведомления в настройках системы.\n\nКак это работает?\nКогда вы получаете уведомление в Dziennik VULCAN, Wulkanowy будет уведомлен (это требует дополнительных прав) и запустит синхронизацию, чтобы отправить свое уведомление.\n\nТОЛЬКО ДЛЯ ПОЛЬЗОВАТЕЛЯ - Upcoming lesson notifications - You must allow the Wulkanowy app to set alarms and reminders in your system settings to use this feature. - Go to settings + Уведомления официального приложения + Захватывать уведомления официального приложения + Удалить уведомления от официального приложения после захвата + Захват уведомлений + С помощью этой функции вы можете получить аналог push-уведомлений из официального приложения. Все, что вам нужно сделать, это разрешить Wulkanowy получать все уведомления в настройках системы.\n\nКак это работает?\nКогда вы получаете уведомление в Dziennik VULCAN, Wulkanowy будет уведомлен (для этого и нужны дополнительные разрешения) и запустит синхронизацию, чтобы отправить свое уведомление.\n\nТОЛЬКО ДЛЯ ПРОДВИНУТЫХ ПОЛЬЗОВАТЕЛЕЙ + Показывать уведомления о предстоящих уроках + Вы должны разрешить Wulkanowy устанавливать будильник и напоминания в настройках системы, чтобы использовать эту функцию. + Перейти к настройкам Синхронизация Автоматическая синхронизация - Приостановить синхронизации во время каникул + Приостановлена во время каникул Интервал синхронизации Только через Wi-Fi Синхронизировать Синхронизировано! Синхронизация не удалась - Идёт синхронизация + Синхронизация... Последняя полная синхронизация: %s Стоимость плюса Стоимость минуса Отвечать с историей сообщений - Показывать среднее арифметическое при отсутствии весов + Показывать среднее арифметическое при отсутствии стоимости Поддержка - Смотреть одиночную рекламу для поддержки проекта + Privacy Policy + Agreements + Consent to processing of data related to ads + Show ads in app + Посмотреть рекламу для поддержки проекта Согласие на обработку данных Для просмотра рекламы вы должны согласиться с условиями обработки данных нашей Политики конфиденциальности - Согласен + Согласиться Политика конфиденциальности - Объявление загружается - Thank you for your support, come back later for more ads + Реклама загружается + Спасибо за вашу поддержку, возвращайтесь позже для дополнительной рекламы + Can we use your data to display ads? + You can change your choice anytime in the app settings. We may use your data to display ads tailored to you or, using less of your data, display non-personalized ads. Please see our Privacy Policy for details + Personalized ads + Non-personalized ads + I am over 18 years old + Yes, personalized ads + Yes, non-personalized ads Расширенные - Внешний вид & Поведение + Внешний вид и поведение Уведомления Синхронизация Реклама Оценки - Панель + Главная Видимость плиток Посещаемость Расписание Оценки - Расчетное среднее + Рассчитанная средняя оценка Сообщения - Внешний вид & Поведение - Языки, темы, темы сортировки темы - Уведомления приложений, проблемы с устранением + Внешний вид и поведение + Язык, тема, сортировка предметов + Настройки уведомлений приложения Уведомления Синхронизация - Автоматическое обновление, интервал синхронизации - Значения плюс и минус, средний расчет + Автоматическая синхронизация и её интервал + Значения плюса и минуса, расчёт средней оценки Расширенные - Версия приложения, участники, социальные порталы - Отображение объявлений, поддержка проекта + Версия приложения, разработчики, соц. сети + Посмотреть рекламу, чтобы поддержать преокт Новые оценки - Новая домашняя работа - Новые конференции - Новые экзамены + Новое домашнее задание + Новые встречи + Новые тесты Счастливый номер - Новые сообщения - Новые заметки - Объявления о новых школах - Показывать push-уведомления - Будущие уроки - Дебаг - Изменение расписания - Новое посещение + Новые письма + Новые замечания + Новые школьные объявления + Push-уведомления + Предстоящие уроки + Отладка + Изменения в расписании + Новая запись о посещаемости Чёрный Красный @@ -746,15 +788,16 @@ Перезапустить Не удалось обновить! Wulkanowy может работать некорректно. Рассмотрите возможность обновления - Нет интернет-подключения - Не удалось подключиться к регистрации. Серверы могут быть перегружены. Пожалуйста, повторите попытку позже - Не удалось загрузить данные. Пожалуйста, повторите попытку позже - Необходимо изменить пароль реестра - Технический перерыв в журнале UONET + продолжается. Попробуйте позже - Неизвестная ошибка UONET + регистр. Попробуйте позже - Неизвестная ошибка приложения. Пожалуйста, повторите попытку позже - Произошла неожиданная ошибка - Функция была выключена школой - Функция не доступна в этом режиме - Это поле является обязательным + Интернет-соединение отсутствует + Произошла ошибка. Проверьте время на вашем устройстве + Не удалось подключиться к дневнику. Возможно, сервера перегружены, повторите попытку позже + Не удалось загрузить данные, повторите попытку позже + Необходимо изменить пароль дневника + UONET+ проводит техническое обслуживание, повторите попытку позже + Неизвестная ошибка дневника UONET+, повторите попытку позже + Неизвестная ошибка приложения, повторите попытку позже + Произошла непредвиденная ошибка + Функция отключена вашей школой + Функция недоступна в режиме Mobile API. Воспользуйтесь другим режимом + Это поле обязательно diff --git a/app/src/main/res/values-sk/preferences_values.xml b/app/src/main/res/values-sk/preferences_values.xml index e64f5606a..e4331315a 100644 --- a/app/src/main/res/values-sk/preferences_values.xml +++ b/app/src/main/res/values-sk/preferences_values.xml @@ -34,6 +34,7 @@ Abecedne Podľa dátumu + Podľa priemeru Dzienniczek+ diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 77e402add..1c6eae8c0 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -12,7 +12,7 @@ O aplikácii Prehliadač protokolov Ladenie - Ladenie upozornení + Ladenie oznámení Tvorcovia Licencie Správy @@ -25,7 +25,7 @@ Podrobnosti účtu Informácie o žiakovi Domov - Centrum upozornení + Centrum oznámení Semester %1$d, %2$d/%3$d @@ -55,7 +55,7 @@ Neplatný symbol Žiak nebol nájdený. Skontrolujte správnosť symbolu a vybrané varianty denníka UONET+ 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 + 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+ Vyberte žiakov, ktorí sa majú do aplikácie prihlásiť Iné možnosti 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í @@ -71,11 +71,15 @@ Obnovte svoj účet Obnoviť Žiak je už prihlásený + Štandardná Manažér účtov Prihlásiť sa Relácia vypršala Relácia vypršala. Prihláste sa prosím znovu + Podpora aplikácie + Páči sa Vám táto aplikácia? Podporte jej vývoj tým, že povolíte neinvazívne reklamy, ktoré môžete kedykoľvek vypnúť + Zapnúť reklamy Známka Semester %d @@ -93,7 +97,7 @@ 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 + Vypočítaný priemer je aritmetický priemer vypočítaný z priemerov predmetov. Umožňuje vám to poznať približný konečný priemer. Vypočítava sa spôsobom zvoleným užívateľom v nastaveniach aplikácii. Odporúča sa vybrať príslušnú možnosť. Dôvodom je rozdielny výpočet školských priemerov. Ak vaša škola navyše uvádza priemer predmetov na stránke denníka Vulcan, aplikácia si ich stiahne a tieto priemery nepočíta. To možno zmeniť vynútením výpočtu priemeru v nastavení aplikácii.\n\nPriemer známok iba z vybraného semestra:\n1. Výpočet váženého priemeru pre každý predmet v danom semestri\n2. Sčítanie vypočítaných priemerov\n3. Výpočet aritmetického priemeru součtených priemerov\n\nPriemer priemerov z oboch semestrov:\n1. Výpočet váženého priemeru pre každý predmet v semestri 1 a 2\n2. Výpočet aritmetického priemeru vypočítaných priemerov za semestre 1 a 2 pre každý predmet.\n3. Sčítanie vypočítaných priemerov\n4. Výpočet aritmetického priemeru součtených priemerov\n\nPriemer známok z celého roka:\n1. Výpočet váženého priemeru za rok pre každý predmet. Konečný priemer v 1. semestri je nepodstatný.\n2. Sčítanie vypočítaných priemerov\n3. Výpočet aritmetického priemeru součtených priemerov 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 @@ -105,9 +109,11 @@ Semester Body Vysvetlivky - Priemer: %1$s + Priemer triedy: %1$s + Váš priemer: %1$s + Vaša známka: %1$s Trieda - Žiák + Žiak %d známka %d známky @@ -204,6 +210,17 @@ Ďalšie lekcie Zobraziť ďalšie lekcie Žiadne informácie o ďalších lekciách + Nová lekcia + Nová ďalšia lekcia + Ďalšia lekcia bola úspešne pridaná + Ďalšia lekcia bola úspešne odstránená + Opakovať každý týždeň + Odstrániť ďalšiu lekciu + Iba táto lekcia + Všetky v sérii + Čas začatia + Čas ukončenia + Čas ukončenia musí byť neskorší ako čas začatia Zhrnutie frekvencií Neprítomnosť zo školských dôvodov @@ -253,10 +270,10 @@ Nové skúšky - Máte %d novú skúšku - Máte %d nové skúšky - Máte %d nových skúšok - Máte %d nových skúšok + %d nová skúška + %d nové skúšky + %d nových skúšok + %d nových skúšok %d skúška @@ -272,13 +289,18 @@ Žiadne správy Od: Komu: - Dátum: %s + Dátum: %1$s Odpoveď Poslať ďalej - Odstrániť - Presunúť do koša + Vybrať všetko + Odznačiť všetko + Presunúť do koša Odstrániť natrvalo Správa bola úspešne odstránená + žiak + rodič + opatrovník + pracovník Zdieľať Vytlačiť Predmet @@ -290,12 +312,11 @@ Iba neprečítané Iba s prílohami Prečítaná: %s - Prečítaná cez: %1$d z %2$d osôb - %d správa - %d správy - %d správ - %d správ + %1$d správa + %1$d správy + %1$d správ + %1$d správ Nová správa @@ -311,6 +332,13 @@ Máte %1$d nových správ Máte %1$d nových správ + + %1$d vybraná + %1$d vybrané + %1$d vybraných + %1$d vybraných + + Správy odstránené Žiadne informácie o poznámkach Body @@ -372,8 +400,8 @@ Žiadne informácie o domácich úlohách - Označiť ako hotové - Nevyrobené + Vykonané + Nevykonané Pridať domácu úlohu Domáca úloha bola úspešně pridaná Domáca úloha bola úspešně odstránená @@ -636,6 +664,7 @@ Skopírované Vrátiť Zmeniť + Pridať do kalendára Žiadne lekcie Vybrať motív @@ -657,21 +686,23 @@ Známky farebnú schému Triedenie predmetov Jazyk - Upozornenia + Oznámenia 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. - Zobraziť upozornenia o ladení + Zobraziť oznámenia + Zobraziť oznámenia o nadchádzajúcich lekciách + Nastaviť oznámenia o nadchádzajúcej lekcií ako trvalé + Vypnúť, keď oznámenia nie je vo vašom hodinkách/náramku viditeľné + Otvoriť systémové nastavenia oznámení + Opravte problémy so synchronizáciou a oznámeniami + Vaše zariadenie môže mať problémy so synchronizáciou dát as oznámeniami.\n\nAk ich chcete opraviť, pridajte Wulkanového do funkcie Autostart a vypnite optimalizáciu/úsporu batérie v nastavení systému telefóne. + Zobraziť oznámenia o ladení Synchronizácia je vypnutá + Oznámenia oficiálnej aplikácie 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 - Upozornenia o nadchádzajúcej lekciu + Odstrániť oznámenia oficiálnej aplikácie po zachytení + Zachytiť oznámení + S touto funkciou môžete získať náhradu push oznámení ako v oficiálnej aplikácii. Všetko, čo musíte urobiť, je povoliť Wulkanowému čítať všetky vaše oznámenia 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é oznámenie.\n\nLEN PRE POKROČILÝCH POUŽĺVATEĹOV + Oznámenia o nadchádzajúcej lekcií Musíte povoliť Wulkanovému nastaviť budíky a pripomenutie v nastavení vášho systému pre použitie tejto funkcie. Prejsť do nastavení Synchronizácia @@ -689,6 +720,10 @@ Odpovedať s históriou správ Vypočítať aritmetický priemer, ak žiadna známka nemá váhu Podpora + Ochrana osobných údajov + Súhlasy + Súhlas so spracovaním údajov súvisiacich s reklamami + Zobraziť reklamy v aplikácii 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 @@ -696,9 +731,16 @@ Ochrana osobných údajov Reklama sa načítava Ďakujeme za vašu podporu, vráťte sa neskôr pre viac reklám + Môžeme použiť Vaše údaje na zobrazenie reklám? + Voľbu môžete kedykoľvek zmeniť v nastavení aplikácie. Môžeme použiť vaše údaje na zobrazenie reklám šitých pre vás alebo pomocou menej vašich dát zobrazovať neprispôsobené reklamy. Podrobnosti nájdete v našich Zásadách ochrany osobných údajov + Prispôsobené reklamy + Neprispôsobené reklamy + Mám ukončené 18 rokov + Áno, prispôsobené reklamy + Áno, neprispôsobené reklamy Pokročilé Vzhľad a správanie - Upozornenia + Oznámenia Synchronizácia Reklamy Známky @@ -711,8 +753,8 @@ Správy Vzhľad a správanie Jazyky, motívy, triedenie predmetov - Upozornenia aplikácie, oprava problémov - Upozornenia + Oznámenia aplikácie, oprava problémov + Oznámenia Synchronizácia Automatická aktualizácia, interval aktualizácií Hodnota plusu a mínusu, výpočet priemeru @@ -728,7 +770,7 @@ Nové správy Nové poznámky Nové školské oznámenia - Push upozornenia + Push oznámenia Nadchádzajúce lekcie Ladenie Zmeny plánu lekcií @@ -747,10 +789,11 @@ Aktualizácia zlyhala! Wulkanowy nemusí fungovať správne. Zvážte aktualizáciu Žiadne internetové pripojenie + Vyskytla sa chyba. Skontrolujte hodiny svojho zariadenia Nedá sa pripojiť ku denníku. Servery môžu byť preťažené. Prosím skúste to znova neskôr Načítanie údajov zlyhalo. Skúste neskôr prosím Je vyžadovaná zmena hesla pre denník - Prebieha údržba UONET+ denník. Skúste to neskôr znova + Prebieha údržba denníka UONET+. Skúste to neskôr znova Neznáma chyba dennika UONET+. Prosím skúste to znova neskôr Neznáma chyba aplikácie. Prosím skúste to znova neskôr Vyskytla sa neočakávaná chyba diff --git a/app/src/main/res/values-uk/preferences_values.xml b/app/src/main/res/values-uk/preferences_values.xml index a8c09bcf8..44acd18e9 100644 --- a/app/src/main/res/values-uk/preferences_values.xml +++ b/app/src/main/res/values-uk/preferences_values.xml @@ -34,6 +34,7 @@ За алфавітом За датою + За середньою Dzienniczek+ @@ -41,24 +42,24 @@ Кольори оцінок в щоденнику - Раз до 1 - Завжди розгорнутий - Необмежена кількість розширень + До 1 за раз + Завжди розгорнуті + Необмежена кількість розгортань - Середні оцінки тільки від обраного семестру - Середнє значення для обох семестів - Середнє оцінювання за весь рік + Середні оцінки тільки за обраний семестр + Середнє значення з обох семестрів + Середня оцінка з цілого року Щасливий номер - Непрочитані повідомлення + Непрочитані листи Відвідуваність Уроки Оцінки Домашні завдання Оголошення школи Тести - Конференції + Зустрічі diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 8543a4708..f90e78e7d 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -8,34 +8,34 @@ Тести Розклад Налаштування - Інше - Про додаток - Переглядач журналів + Більше + Про + Переглядач логів Відладка - Налагодження сповіщень + Відладка сповіщень Розробники Ліцензії - Повідомлення - Нове повідомлення - Нова домашня робота - Нотатки та досягнення + Листи + Новий лист + Нове домашнє завдання + Зауваження та похвали Домашні завдання - Менеджер аккаунтів - Виберіть обліковий запис + Змінити облікові записи + Вибрати обліковий запис Деталі облікового запису Інформація про учня - Дошка - Журнал сповіщень + Головна + Центр сповіщень %1$d семестр, %2$d/%3$d - Авторизуйтеся за допомогою аккаунта учня або батьків - Введіть символ зі сторінки реєстру: <b>%1$s</b> + Авторизуйтеся за допомогою облікового запису учня або батьків + Введіть symbol зі сторінки щоденника: <b>%1$s</b> Ім\'я користувача - Електронна пошта - Логін, PESEL або електронна пошта + E-mail + Логін, PESEL або e-mail Пароль - UONET + варіант реєстрації + Тип щоденника UONET+ Мobile API Scraper Hybrid @@ -45,37 +45,41 @@ Увійти Занадто короткий пароль Вказані невірні дані - %1$s. Переконайтеся, що обрано правильну варіацію запису UONET+ + %1$s. Переконайтеся, що обрано правильний тип щоденника UONET+ Неправильний PIN Неправильний token - Минув термін дії токену - Недійсна адреса електронної пошти - Використовуйте призначений логін замість електронної пошти - Використовуйте призначений логін або електронну адресу в @%1$s - Неправильний симбвол - Студента не знайдено Перевірте символ та обраний варіант реєстру UONET+ - Даного учня вже авторизовано - Символ можна знайти на сторінці реєстру в   Учень →   Мобільний доступ →   Додайте мобільне приладдя .\n\nПереконайтесь, що ви встановили відповідний варіант реєстру в полі UONET + варіант реєстрації на попередньому екрані. На даний момент Wulkanowy не виявляє учнів дошкільних закладів + Token згаснув + Недійсна адреса e-mail + Використовуйте призначений логін замість адреси e-mail + Використовуйте призначений логін або адресу e-mail в @%1$s + Неправильний symbol + Студента не знайдено. Перевірте symbol та обранний тип щоденника UONET+ + Цього учня вже авторизовано + Symbol можна знайти на сторінці реєстрації в  Uczeń→ Dostęp Mobilny → Zarejestruj urządzenie mobilne.\n\nПереконайтесь, що ви встановили відповідний тип щоденника в полі Тип щоденника UONET+ на попередньому екрані Виберіть учнів для авторизації в додатку Інші варіанти - У цьому режимі не працюють: щасливий номер, статистика класу по оцінкам, статистика відвідуваності і уроків, інформація про школу і список зареєстрованних пристроїв + У цьому режимі не працюють: щасливий номер, статистика класу по оцінкам, статистика відвідуваності та уроків, інформація про школу та список зареєстрованих пристроїв Scraper - режим, котрий показує дані так само, як і сторінка щоденника - Hybrid - це комбінація найкращих функцій інших двох режимів. Він працює швидше, ніж Scraper, и впроваджує функції, котрих нема в режимі Mobile API. Знаходиться в експериментальній стадії - Політика приватності + Hybrid - це комбінація найкращих функцій інших двох режимів. Він працює швидше, ніж Scraper, й впроваджує функції, котрих нема в режимі Mobile API. Знаходиться в експериментальній стадії + Політика конфіденційності Проблеми з авторизацією? Зв\'яжіться з нами! - Електронна пошта + E-mail Discord - Відправити лист - Переконайтеся, що ви вибрали правильний варіант реєстрації UONET+! + Надіслати електронний лист + Переконайтеся, що ви вибрали правильний тип щоденника UONET+! Забули пароль? Відновіть свій обліковий запис Відновити - Учень вже увійшов до системи + Учня вже авторизовано + Стандартний - Менеджер аккаунтів + Змінити облікові записи Увійти Минув термін дії сесії Минув термін дії сесії, авторизуйтеся знову + Application support + Do you like this app? Support its development by enabling non-invasive ads that you can disable at any time + Enable ads Оцінка %d семестр @@ -86,26 +90,28 @@ Коментар Кількість нових оцінок: %1$d Середня оцінка: %1$.2f - Балів: %s - Відсутність середньої оцінки - Сума балів + Бали: %s + Середня оцінка відсутня + Всього балів Підсумкова оцінка - Очікувана оцінка + Передбачувана оцінка Розрахована середня оцінка - Як розраховується середньо? - Розрахункове середнє - це середнє арифметичне, обчислене з середніх показників для випробуваних. Це дозволяє дізнатися приблизну кінцеву середню величину. Він розраховується способом, обраним користувачем у налаштуваннях програми. Рекомендується вибрати відповідний варіант. Це пояснюється тим, що розрахунок середніх показників за школою відрізняється. Крім того, якщо у вашій школі повідомляється середнє значення предметів на сторінці Вулкан, програма завантажує їх і не обчислює ці середні значення. Це можна змінити шляхом примусового розрахунку середнього значення в налаштуваннях програми.\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. Поділіть на кількість предметів, які вже оцінені + Як працює \"Розрахована середня оцінка\"? + The Calculated Average is the arithmetic average calculated from the subjects averages. It allows you to know the approximate final average. It is calculated in a way selected by the user in the application settings. It is recommended that you choose the appropriate option. This is because the calculation of school averages differs. Additionally, if your school reports the average of the subjects on the Vulcan page, the application downloads them and does not calculate these averages. This can be changed by forcing the calculation of the average in the application settings.\n\nAverage of grades only from selected semester:\n1. Calculating the weighted average for each subject in a given semester\n2.Adding calculated averages\n3. Calculation of the arithmetic average of the summed averages\n\nAverage of averages from both semesters:\n1.Calculating the weighted average for each subject in semester 1 and 2\n2. Calculating the arithmetic average of the calculated averages for semesters 1 and 2 for each subject.\n3. Adding calculated averages\n4. Calculation of the arithmetic average of the summed averages\n\nAverage of grades from the whole year:\n1. Calculating weighted average over the year for each subject. The final average in the 1st semester is irrelevant.\n2. Adding calculated averages\n3. Calculating the arithmetic average of summed averages + Як працює \"Підсумкова середня оцінка\"? + Підсумкове середнє значення - це середнє арифметичне, обчислене з усіх наявних наразі підсумкових оцінок у даному семестрі. \n\nСхема обчислення складається з таких кроків:\n1. Сумування підсумкових оцінок, виставленних викладачами\n2. Ділення на кількість предметів, з яких виставлені ці оцінки Підсумкова середня оцінка - з %1$d із %2$d тем + %1$d із %2$d предметів Підсумок Клас Позначити як прочитане - Поточні + Часткові Семестрові Бали Умовні позначення - Середня оцінка: %1$s + Середня класу: %1$s + Ваша середня: %1$s + Ваша оцінка: %1$s Клас Учень @@ -121,10 +127,10 @@ Нові оцінки - Нова прогнозована оцінка - Нові прогнозовані оцінки - Нові прогнозовані оцінки - Нові прогнозовані оцінки + Нова передбачувана оцінка + Нові передбачувані оцінки + Нові передбачувані оцінки + Нові передбачувані оцінки Нова підсумкова оцінка @@ -133,16 +139,16 @@ Нові підсумкові оцінки - Ви отримали %1$d оцінку - Ви отримали %1$d оцінки - Ви отримали %1$d оцінок - Ви отримали %1$d оцінок + Ви отримали %1$d нову оцінку + Ви отримали %1$d нові оцінки + Ви отримали %1$d нових оцінок + Ви отримали %1$d нових оцінок - Ви отримали %1$d нову прогнозовану оцінку - Ви отримали %1$d нові прогнозовані оцінки - Ви отримали %1$d нових прогнозованих оцінок - Ви отримали %1$d нових прогнозованих оцінок + Ви отримали %1$d нову передбачувану оцінку + Ви отримали %1$d нові передбачувані оцінкі + Ви отримали %1$d нових передбачуваних оцінок + Ви отримали %1$d нових передбачуваних оцінок Ви отримали %1$d нову підсумкову оцінку @@ -156,62 +162,73 @@ Група Години Зміни - Брак уроків у цей день + Немає уроків у цей день %s хвилин %s сек - %1$s лишилося + %1$s залишилося через %1$s - Завершено + Закінчено Зараз: %s Наступний: %s Пізніше: %s %1$s урок %2$d - %3$s - Зміна місця з %1$s на %2$s - Змінити вчителя з %1$s на %2$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$s - %2$d зміни в розкладі + %1$s - %2$d змін в розкладі + %1$s - %2$d змін у розкладі %1$d зміна в розкладі %1$d зміна в розкладі %1$d зміна в розкладі - %1$d змін у розкладі + %1$d змін в розкладі %d зміна - %d зміна - %d зміна + %d зміни + %d змін %d змін Уроки, що відбулися - Показати уроки, що відбулися - Брак інформації о уроках, що відбулися + Показати завершені уроки + Немає інформації про завершенні уроки Тема Відсутність Ресурси Додаткові уроки Показати додаткові уроки - Немає інформації про додаткових уроків + Немає інформації про додаткові уроки + Новий урок + Новий додатковий урок + Додатковий урок успішно додано + Успішно видалено додатковий урок + Повторювати щотижня + Видалити додатковий урок + Тільки цей урок + Всі в серії + Час початку + Час завершення + Час завершення має бути більшим, ніж час початку Підсумок відвідуваності Відсутність зі шкільних причин Відсутність з поважних причин - Відсутність з не поважних причин + Відсутність без поважних причин Звільнення Спізнення з поважних причин - Спізнення з не поважних причин + Спізнення без поважних причин Присутність Вилучено Невідомо @@ -223,22 +240,22 @@ Оберіть хоча б одну відсутність Змінити статус - Нова відвідуваність - Нова відвідуваність - Нова відвідуваність - Нова відвідуваність + Новий запис відвідуваності + Нові записи відвідуваності + Нові записи відвідуваності + Нові записи відвідуваності - %1$d новий відвідувач - %1$d новий відвідувач - %1$d новий відвідувач - %1$d відвідування + %1$d новий запис відвідуваності + %1$d нові записи відвідуваності + %1$d нові записи відвідуваності + %1$d нові записи відвідуваності - %d відвідування - %d відвідування - %d відвідування - %d відвідування + %d запис відвідуваності + %d записи відвідуваності + %d записів відвідуваності + %d записів відвідуваності Загальна @@ -247,72 +264,83 @@ Тип Дата запису - Новий іспит - Новий іспит - Новий іспит - Нові іспити + Новий тест + Нові тести + Нові тести + Нові тести - Ви отримали %d новий іспит - Ви отримали %d новий іспит - Ви отримали %d новий іспит - Ви отримали %d нових іспитів + %d новий тест + %d нових тести + %d нових тестів + %d нових тестів - %d екзамен - %d екзамен - %d екзамен - %d іспити + %d тест + %d тести + %d тести + %d тести Отримані Відправлені Кошик (брак теми) - Нема повідомлень + Немає листів Від: Кому: - Дата: %s + Дата: %1$s Відповісти Переслати - Видалити - Перемістити у кошик + Вибрати всі + Відмінити вибір + Перемістити до кошика Видалити назавжди - Повідомлення було успішно видалено - Поділіться + Лист було успішно видалено + student + parent + guardian + employee + Поділитись Друк Тема Зміст - Повідомлення було успішно відправлено - Такого повідомлення не існує + Лист було успішно відправлено + Такого листа не існує Необхідно обрати принаймні 1 адресата - Зміст повідомлення мусить складатися принаймні з 3 знаків + Зміст листа повинен складатися принаймні з 3 знаків Лише непрочитані Тільки з вкладеннями - Читання: %s - Прочитанно:%1$d через %2$d людей + Прочитаний: %s - %d повідомлення - %d повідомлення - %d повідомлень - %d повідомлень + %1$d лист + %1$d листи + %1$d листів + %1$d листів - Нове повідомлення - Нові повідомлення - Нові повідомлення - Нові повідомлення + Новий лист + Нові листи + Нові листи + Нові листи - Ви хочете відновити повідомлення в чернетці? - Ви хочете відновити чернетку повідомлення з отримувачами: %s? + Ви хочете відновити чорновик листа? + Ви хочете відновити чорновик листа з отримувачами: %s? - Ви отримали %1$d нове повідомлення - Ви отримали %1$d нових повідомлення - Ви отримали %1$d нових повідомлень - Ви отримали %1$d нових повідомлень + Ви отримали %1$d новий лист + Ви отримали %1$d нові листи + Ви отримали %1$d нових листів + Ви отримали %1$d нових листів + + %1$d вибрано + %1$d вибрано + %1$d вибрано + %1$d вибрано + + Листи видалені - Брак інформації о зауваженнях + Немає інформації о зауваженнях Бали %d зауваження @@ -321,20 +349,20 @@ %d зауважень - Нова нотатка - Нові нотатки - Нових нотаток - Нових нотаток + Нове зауваження + Нові зауваження + Нові зауваження + Нові зауваження - %1$d нове зауваження - %1$d нових зауваження - %1$d нових зауважень - %1$d нових зауважень + Ви отримали %1$d нове зауваження + Ви отримали %1$d нові зауваження + Ви отримали %1$d нових зауважень + Ви отримали %1$d нових зауважень - %d похвалили + %d похвал %d похвали %d похвал %d похвал @@ -353,62 +381,62 @@ - %d нейтральна нота - %d нейтральні ноти - %d нейтральних нот - %d нейтральних нот + %d нейтральний запис + %d нейтральні записи + %d нейтральних записів + %d нейтральних записів - Нова нейтральна нота - Нова нейтральна нота - Нова нейтральна нота - Нові нейтральні ноти + Новий нейтральний запис + Нові нейтральні записи + Нові нейтральні записи + Нові нейтральні записи - Ви отримали %1$d нову нейтральну ноту - Ви отримали %1$d нові нейтральні ноти - Ви отримали %1$d нових нейтральних нот - Ви отримали %1$d нових нейтральних нот + Ви отримали %1$d новий нейтральний запис + Ви отримали %1$d нові нейтральні записи + Ви отримали %1$d нових нейтральних записів + Ви отримали %1$d нових нейтральних записів - Брак домашніх завдань - Позначити як зроблене - Позначити як не зроблене - Додати домашню роботу - Домашню роботу додано успішно - Домашню роботу видалено успішно - Додатки + Немає домашніх завдань + Зроблено + Не зроблено + Додати домашнє завдання + Домашнє завдання додано успішно + Домашнє завдання видалено успішно + Вкладення - Нова домашня робота - Нова домашня робота - Нова домашня робота - Нова домашня робота + Нове домашнє завдання + Нові домашні завдання + Нові домашні завдання + Нові домашні завдання - Ви отримали %d нове домашнє завдання - Ви отримали %d нове домашнє завдання - Ви отримали %d нове домашнє завдання - Ви отримали %d нове домашнє завдання + Ви отримали %d домашнє завдання + Ви отримали %d домашні завдання + Ви отримали %d домашніх завдань + Ви отримали %d домашніх завдань %d домашнє завдання - %d домашнє завдання - %d домашнє завдання - %d домашнє завдання + %d домашні завдання + %d домашні завдання + %d домашні завдання Щасливий номер Сьогоднішній щасливий номер - Брак інформації о щасливому номері + Немає інформації о щасливому номері Сьогоднішній щасливий номер Сьогоднішній щасливий номер: %s Показати історію - Історія щасливих чисел + Історія щасливих номерів Немає інформації про щасливі номери Мобільні пристрої - Брак пристроїв + Немає пристроїв Видалити Пристрій видалено QR-код @@ -419,98 +447,98 @@ Школа та вчителі Школа - Брак інформації про школу + Немає інформації про школу Назва школи Адреса школи Телефон Директор - Викладач + Педагог Показати на мапі - Зателефонувати + Задзвонити Вчителі - Брак інформації про вчителів - Брак предмету + Немає інформацій про вчителів + Немає предмету Зустрічі Немає інформації про зустрічі - %d конференція - %d конференція - %d конференція - %d конференцій + %d зустріч + %d зустрічі + %d зустрічей + %d зустрічей - Нова конференція - Нова конференція - Нова конференція - Нові конференції + Нова зустріч + Нові зустрічі + Нові зустрічі + Нові зустрічі - У вас є %1$d нова конференція - У вас є %1$d нова конференція - У вас є %1$d нова конференція - У вас є %1$d нових конференцій + У вас є %1$d нова зустріч + У вас є %1$d нові зустрічі + У вас є %1$d нових зустрічей + У вас є %1$d нових зустрічей - Присутність на конференції + Присутність на зустрічі Порядок денний Оголошення школи - Жодних навчальних оголошень + Немає шкільних оголошень - %d оголошення про школу - %d оголошення про школу - %d оголошення про школу - Оголошення нової школи + %d шкільне оголошення + %d шкільних оголошення + %d шкільних оголошень + %d шкільних оголошень - Оголошення нової школи - Оголошення нової школи + Нове шкільне оголошення + Нові шкільні оголошення Нові шкільні оголошення Нові шкільні оголошення - У вас є %1$d нове оголошення про школу - У вас є %1$d нове оголошення про школу - У вас є %1$d нове оголошення про школу - У вас є %1$d нових оголошень про школи + У вас є %1$d нове шкільне оголошення + У вас є %1$d нові шкільні оголошення + У вас є %1$d нових шкільних оголошень + У вас є %1$d нових шкільних оголошень - Додати аккаунт + Додати обліковий запис Вийти - Ви впевнені, що хочете вийти з цього аккаунту? - Вийти з аккаунту учня - Студентський рахунок - Головний рахунок - Редагувати дані - Менеджер аккаунтів - Виберіть учня + Ви впевнені, що хочете вийти з цього облікового запису? + Вийти з облікового запису учня + Обліковий запис учня + Обліковий запис батька + Змінити дані + Змінити облікові записи + Вибрати учня Сім\'я Контакт Деталі проживання - Особиста інформація + Персональні дані Версія додатка Розробники - Список розробників \"Wulkanowy\" + Список розробників додатку Виникла помилка? Повідомити о помилці за допомогою e-mail FAQ - Запитання, які часто задають + Найбільш поширенні питання Сервер Discord - Приєднатися до спільноти додатка - Фен-сторінка Facebook - Сторінка Twitter + Приєднуйтеся до спільноти додатка + Сторінка у Facebook + Сторінка у Twitter Стежте за нами у Твіттері - Вподобати нашу фансторінку у Facebook + Вподобайте нас у Facebook Політика конфіденційності Правила зберігання особистих даних Налаштування системи Відкрити налаштування системи Домашня сторінка - Допомогти розвитку додатка + Відвідайте наш сайт і допоможіть в розробці додатку Ліцензії - Ліцензії вжитих бібліотек + Ліцензії використаних бібліотек Ліцензія @@ -522,12 +550,12 @@ Друге ім\'я Стать Польське громадянство - Прізвище + Дошлюбне прізвище Імена батька і матері - Номер телефону + Стаціонарний телефон Мобільний телефон - Електронна пошта - Місця проживання + E-mail + Місце проживання Адреса реєстрації Адреса для кореспонденції Прізвище та ім\'я @@ -535,9 +563,9 @@ Адреса Телефони Чоловіча - Жінка + Жіноча Прізвище - Охоронець + Опікун Псевдонім Додати псевдонім @@ -548,73 +576,73 @@ Уроки (Завтра) - (сьогодні та завтра) + (Сьогодні та завтра) Через мить: Незабаром: - Перше: + Перший: Зараз: Кінець уроків Далі: - Пізніше : + Пізніше: - %1$d більше уроку - %1$d більше уроку - %1$d більше уроку - %1$d більше уроків + ще %1$d урок + ще %1$d уроки + ще %1$d уроків + ще %1$d уроків до %1$s Немає майбутніх уроків Сталася помилка під час завантаження уроків - Домашня робота + Домашнє завдання Немає домашнього завдання для виконання - Помилка при завантаженні домашньої роботи + Помилка при завантаженні домашніх завдань - Ще %1$d домашнє завдання - Ще %1$d домашнє завдання - Ще %1$d домашнє завдання - Ще %1$d домашнє завдання + ще %1$d домашнє завдання + ще %1$d домашніх завдання + ще %1$d домашніх завдань + ще %1$d домашніх завдань до %1$s - Останні оцінки + Остатні оцінки Немає нових оцінок - Помилка при завантаженні класів - Оголошення школи + Помилка при завантаженні оцінок + Шкільні оголошення Немає поточних оголошень Помилка при завантаженні анонсів - %1$d нових оголошень - %1$d нових оголошень - %1$d нових оголошень - %1$d нових оголошень + ще %1$d оголошення + ще %1$d оголошення + ще %1$d оголошень + ще %1$d оголошень - Іспити - Немає майбутніх іспитів - Помилка при завантаженні іспитів + Тести + Немає майбутніх тестів + Помилка при завантаженні тестів - %1$d ще екзамен - %1$d ще екзамен - %1$d ще екзамен - %1$d ще іспитів + ще %1$d тест + ще %1$d тести + ще %1$d тестів + ще %1$d тестів - Конференції - Немає майбутніх конференцій - Помилка при завантаженні конференцій + Зустрічі + Немає майбутніх зустрічей + Помилка при завантаженні зустрічей - Ще %1$d конференція - Ще %1$d конференція - Ще %1$d конференція - %1$d більше конференцій + ще %1$d зустріч + ще %1$d зустрічі + ще %1$d зустрічей + ще %1$d зустрічей - Помилка при завантаженні даних + Виникла помилка при завантаженні даних Нічого - Провірити наявність оновлень - Перед тим, як повідомлювати о помілці, перевірте наявність оновлень + Перевірити наявність оновлень + Перед тим, як повідомлювати о помилці, перевірте наявність оновлень Зміст Повторити Опис - Брак опису + Немає опису Вчитель Дата Дата запису @@ -631,25 +659,26 @@ Так Ні Зберегти - Титул + Заголовок Додати Скопійовано Відмінити Змінити + Додати у календар - Брак уроків + Немаэ уроків Увібрати тему Яскрава Темна Тема системи - Додатки + Додаток Вікно за замовчуванням - Розрахункові середні параметри - Примусово розрахувати середню оцінку через додаток - Показати присутність + Параметри розраховування середніх оцінок + Примусово розраховувати середню оцінку через додаток + Показувати присутність Тема - Розширення оцінок + Розгортання оцінок Позначити поточний урок Показувати групи поруч з темами Показувати діаграми в оцінках класу @@ -662,99 +691,113 @@ Показувати повідомлення Показувати повідомлення о наступних уроках Зробити сповіщення майбутнього уроку нестійкими - Вимкнути коли сповіщення не показуються у відстежувачі/темпі + Вимкніть, якщо сповіщення не показуються на розумному годиннику Відкрити налаштування сповіщень системи Виправити помилки з синхронізацією і повідомленнями - На вашому пристрої можуть бути помилки з синхронізацією і повідомленнями\n\nЩоб виправити іх, вам необхідно додати Wulkanowy в авто-старт и вимкнути оптимізацію/экономію батареї в налаштуваннях пристрою. + На вашому пристрої можуть бути помилки з синхронізацією і повідомленнями\n\nЩоб виправити іх, вам необхідно додати Wulkanowy в авто-старт і вимкнути оптимізацію/экономію батареї в налаштуваннях пристрою. Показувати дебаг-повідомлення Синхронізація вимкнена - Захоплювати офіційні сповіщення програм - Показувати push-повідомлення - За допомогою цієї функції ви можете отримати заміну push -повідомлень, як у офіційному додатку. Все, що вам потрібно зробити, це дозволити Wulkanowy отримувати всі сповіщення у налаштуваннях вашої системи. \ N \ nЯк це працює? \ NКоли ви отримаєте сповіщення у Dziennik VULCAN, Wulkanowy отримає сповіщення (для цього призначені ці додаткові дозволи) і запустить синхронізація, яка може надсилати власне сповіщення. \ n \ n ТІЛЬКИ ДЛЯ РОЗШИРЕНИХ КОРИСТУВАЧІВ - Upcoming lesson notifications - You must allow the Wulkanowy app to set alarms and reminders in your system settings to use this feature. - Go to settings + Сповіщення офіційного додатку + Захоплювати сповіщення офіційного додатку + Видалити сповіщення офіційного додатку після захоплення + Захоплювати сповіщення + За допомогою цієї функції ви можете отримати аналог повідомлень з офіційного додатку. Все, що вам потрібно зробити, це дозволити Wulkanowy отримувати всі сповіщення у налаштуваннях вашої системи.\n\nЯк це працює?\nКоли ви отримаєте сповіщення у Dziennik VULCAN, Wulkanowy отримає сповіщення (для цього призначені ці додаткові дозволи) і запустить синхронізацію, щоб надіслати своє власне повідомлення.\n\nТІЛЬКИ ДЛЯ ПРОСУНУТИХ КОРИСТУВАЧІВ + Показувати повідомлення о наступних уроках + Ви повинні дозволити Wulkanowy встановлювати будильник та нагадування у налаштуваннях вашої системи для використання цієї функції. + Перейти до налаштувань Синхронізація - Автоматична синхронізація + Автоматичне оновлення Призупинено на час канікул Інтервал оновлення Тільки через Wi-Fi Синхронізувати Синхронізовано! Синхронізація не вдалася - Триває синхронізація + Синхронізація... Остання синхронізація: %s Вартість плюсу - Вага мінуса + Вартість мінуса Відповісти з історією повідомлень - Показувати в середньому арифметику, якщо немає ваги + Вилічити середню аритметичну, якщо оцінка немає вартості Підтримка - Відстежуйте єдину рекламу для підтримки проекту + Privacy Policy + Agreements + Consent to processing of data related to ads + Show ads in app + Подивіться одну рекламу для підтримки проєкту Згода в обробці даних Щоб переглянути рекламу, ви повинні погодитися з умовами обробки даних нашої Політики конфіденційності Погоджуюсь Політика конфіденційності Реклама завантажується - Thank you for your support, come back later for more ads + Дякуємо за вашу підтримку, повертайтеся пізніше для більшої кількості реклам + Can we use your data to display ads? + You can change your choice anytime in the app settings. We may use your data to display ads tailored to you or, using less of your data, display non-personalized ads. Please see our Privacy Policy for details + Personalized ads + Non-personalized ads + I am over 18 years old + Yes, personalized ads + Yes, non-personalized ads Додатково - Вигляд & Поведінка - Повідомлення + Вигляд та поведінка + Сповіщення Синхронізація Реклама Оцінки - Дошка + Головна Видимість плиток Відвідуваність Розклад - Класи - Обчислена середня + Оцінки + Розрахована середня оцінка Повідомлення - Вигляд & Поведінка - Мови, теми, тема сортування - Сповіщення додатку, виправляти проблеми - Повідомлення + Вигляд та поведінка + Мова, тема, сортування предметів + Налаштуванная сповіщень додатку + Сповіщення Синхронізація - Автоматичне оновлення, інтервал синхронізації - Плюс і мінус значення, середні обчислення + Автоматичне оновлення та його інтервал + Вартість плюса та мінуса, розраховування середньої Додатково - Версія програми, учасники, соціальні портали - Відображається реклама, підтримка проектів + Версія додатку, учасники, соціальні портали + Показування реклами, підтримка проєкту Нові оцінки - Нова домашня робота - Нові конференції - Нові іспити + Нові домашні завдання + Нові зустрічі + Нові тести Щасливий номер Нові повідомлення - Нові нотатки - Оголошення нової школи - Показувати push-повідомлення + Нові зауваження + Нові шкільні оголошення + Показувати сповіщення від розробників Наступні уроки - Дебаг - Зміна у розкладі - Нова відвідуваність + Відладка + Зміни у розкладі + Нові записи відвідуваності Чорний Червоний Голубий Зелений Фіолетовий - Брак кольору + Без кольору Завантаження оновлень розпочато… Щойно завантажено оновлення. - Перезапустити + Перезавантажити Помилка оновлення! Wulkanowy може не працювати належним чином. Подумайте про оновлення - Брак з\'єднання з інтернетом - Помилка підключення до реєстрації. Сервери можуть бути перевантажені. Будь-ласка спробуйте пізніше - Помилка завантаження даних. Будь-ласка спробуйте пізніше - Потрібна реєстрація зміни пароля - Технічна перерва в журналі UONET + продовжується. Спробуйте пізніше - Невідома помилка реєстру UONET+. Спробуйте ще раз пізніше - Невідома помилка програми. Будь-ласка спробуйте пізніше + Немає з\'єднання з інтернетом + Сталася помилка. Перевірте годинник пристрою + Помилка підключення до щоденнику. Сервери можуть бути перевантажені, спробуйте пізніше + Помилка завантаження даних, спробуйте пізніше + Необхідна зміна пароля щоденника + UONET+ проводить технічне осблуговування, спробуйте пізніше + Невідома помилка щоденника UONET+, спробуйте пізніше + Невідома помилка програми, спробуйте пізніше Відбулася несподівана помилка - Функція вимкнена школою - Функція не доступна в цьому режимі - Це поле обов\'язкове для заповнення + Функція вимкнена вашою школою + Функція недоступна в режимі Mobile API. Увійдіть в інший режим + Це поле обовʼязкове diff --git a/app/src/main/res/values/api_hosts.xml b/app/src/main/res/values/api_hosts.xml index 158490471..8413d68e4 100644 --- a/app/src/main/res/values/api_hosts.xml +++ b/app/src/main/res/values/api_hosts.xml @@ -1,7 +1,7 @@ - Vulcan + @string/login_host_standard Opolska eSzkoła Gdańska Platforma Edukacyjna Lubelski Portal Oświatowy diff --git a/app/src/main/res/values/preferences_defaults.xml b/app/src/main/res/values/preferences_defaults.xml index 7fb3d5c05..9c092ec5e 100644 --- a/app/src/main/res/values/preferences_defaults.xml +++ b/app/src/main/res/values/preferences_defaults.xml @@ -28,6 +28,7 @@ false 0 false + false LUCKY_NUMBER MESSAGES @@ -36,4 +37,6 @@ GRADES ANNOUNCEMENTS + false + false diff --git a/app/src/main/res/values/preferences_keys.xml b/app/src/main/res/values/preferences_keys.xml index fef062dd1..80a71bc71 100644 --- a/app/src/main/res/values/preferences_keys.xml +++ b/app/src/main/res/values/preferences_keys.xml @@ -31,9 +31,13 @@ homework_fullscreen subjects_without_grades optional_arithmetic_average - message_send_is_draft - message_send_recipients + message_draft last_sync_date notifications_piggyback + notifications_piggyback_cancel_original single_ad_support + ads_enabled + ads_privacy_policy + ads_consent_data_processing + ads_over_eighteen diff --git a/app/src/main/res/values/preferences_values.xml b/app/src/main/res/values/preferences_values.xml index 1d777bdb6..312f0b878 100644 --- a/app/src/main/res/values/preferences_values.xml +++ b/app/src/main/res/values/preferences_values.xml @@ -82,10 +82,12 @@ Alphabetically By date + By average alphabetic date + average diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 97c8f0345..e4542547d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -59,7 +59,7 @@ Invalid symbol Student not found. Validate the symbol and the chosen variation of the UONET+ register 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 + 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 Select students to log in to the application Other options In this mode, a lucky number does not work, a class grade stats, summary of attendance, excuse for absence, completed lessons, school information and preview of the list of registered devices @@ -71,12 +71,13 @@ 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\nNazwa szkoły wraz z miejscowością i numer klasy: + 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\nNazwa szkoły i miejscowość: Make sure you select the correct UONET+ register variation! I forgot my password Recover your account Recover Student is already signed in + Standard @@ -84,6 +85,9 @@ Log in Session expired Session expired, log in again + Application support + Do you like this app? Support its development by enabling non-invasive ads that you can disable at any time + Enable ads @@ -103,7 +107,7 @@ 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 + The Calculated Average is the arithmetic average calculated from the subjects averages. It allows you to know the approximate final average. It is calculated in a way selected by the user in the application settings. It is recommended that you choose the appropriate option. This is because the calculation of school averages differs. Additionally, if your school reports the average of the subjects on the Vulcan page, the application downloads them and does not calculate these averages. This can be changed by forcing the calculation of the average in the application settings.\n\nAverage of grades only from selected semester:\n1. Calculating the weighted average for each subject in a given semester\n2.Adding calculated averages\n3. Calculation of the arithmetic average of the summed averages\n\nAverage of averages from both semesters:\n1.Calculating the weighted average for each subject in semester 1 and 2\n2. Calculating the arithmetic average of the calculated averages for semesters 1 and 2 for each subject.\n3. Adding calculated averages\n4. Calculation of the arithmetic average of the summed averages\n\nAverage of grades from the whole year:\n1. Calculating weighted average over the year for each subject. The final average in the 1st semester is irrelevant.\n2. Adding calculated averages\n3. Calculating the arithmetic average of summed averages 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 @@ -115,7 +119,9 @@ Semester Points Legend - Average: %1$s + Class average: %1$s + Your average: %1$s + Your grade: %1$s Class Student @@ -198,6 +204,17 @@ Additional lessons Show additional lessons No info about additional lessons + New lesson + New additional lesson + Additional lesson added successfully + Additional lesson deleted successfully + Repeat weekly + Delete additional lesson + Just this lesson + All in the series + Start time + End time + End time must be greater than start time @@ -264,13 +281,18 @@ No messages From: To: - Date: %s + Date: %1$s Reply Forward - Delete - Move to trash + Select all + Unselect all + Move to trash Delete permanently Message deleted successfully + student + parent + guardian + employee Share Print Subject @@ -282,10 +304,9 @@ Only unread Only with attachments Read: %s - Read by: %1$d of %2$d people - %d message - %d messages + %1$d message + %1$d messages New message @@ -297,6 +318,11 @@ You received %1$d message You received %1$d messages + + %1$d selected + %1$d selected + + Messages deleted @@ -624,6 +650,7 @@ Copied Undo Change + Add to calendar @@ -661,7 +688,9 @@ 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. Show debug notifications Synchronization is disabled + Official app notifications Capture official app notifications + Remove official app notifications after capture 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 Upcoming lesson notifications @@ -685,6 +714,10 @@ Show arithmetic average when no weights provided Support + Privacy Policy + Agreements + Consent to processing of data related to ads + Show ads in app 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 @@ -692,6 +725,13 @@ Privacy policy Ad is loading Thank you for your support, come back later for more ads + Can we use your data to display ads? + You can change your choice anytime in the app settings. We may use your data to display ads tailored to you or, using less of your data, display non-personalized ads. Please see our Privacy Policy for details + Personalized ads + Non-personalized ads + I am over 18 years old + Yes, personalized ads + Yes, non-personalized ads Advanced Appearance & Behavior @@ -754,6 +794,7 @@ No internet connection + An error occurred. Check your device clock Connection to register failed. Servers can be overloaded. Please try again later Loading data failed. Please try again later Register password change required diff --git a/app/src/main/res/xml/scheme_preferences_notifications.xml b/app/src/main/res/xml/scheme_preferences_notifications.xml index 442581bfd..0366914ac 100644 --- a/app/src/main/res/xml/scheme_preferences_notifications.xml +++ b/app/src/main/res/xml/scheme_preferences_notifications.xml @@ -16,9 +16,9 @@ app:title="@string/pref_notify_upcoming_lessons_switch" /> @@ -31,13 +31,24 @@ + app:title="@string/pref_notify_notifications_piggyback_header"> + + + (getString(R.string.pref_key_ads_privacy_policy))?.setOnPreferenceClickListener { + presenter.onPrivacySelected() + true + } + + findPreference(getString(R.string.pref_key_ads_consent_data_processing)) + ?.setOnPreferenceChangeListener { _, newValue -> + presenter.onConsentSelected(newValue as Boolean) + true + } } override fun showAd(ad: RewardedInterstitialAd) { @@ -45,13 +59,50 @@ class AdsFragment : PreferenceFragmentCompat(), MainView.TitledView, AdsView { } override fun showPrivacyPolicyDialog() { - MaterialAlertDialogBuilder(requireContext()) - .setTitle(getString(R.string.pref_ads_privacy_title)) - .setMessage(getString(R.string.pref_ads_privacy_description)) - .setPositiveButton(getString(R.string.pref_ads_privacy_agree)) { _, _ -> presenter.onAgreedPrivacy() } - .setNegativeButton(android.R.string.cancel) { _, _ -> } - .setNeutralButton(getString(R.string.pref_ads_privacy_link)) { _, _ -> presenter.onPrivacySelected() } + val dialogAdsConsentBinding = DialogAdsConsentBinding.inflate(layoutInflater) + + val dialog = MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.pref_ads_consent_title) + .setMessage(R.string.pref_ads_consent_description) + .setView(dialogAdsConsentBinding.root) + .setOnCancelListener { presenter.onPrivacyDialogCanceled() } .show() + + dialogAdsConsentBinding.adsConsentOver.setOnCheckedChangeListener { _, isChecked -> + dialogAdsConsentBinding.adsConsentPersonalised.isEnabled = isChecked + } + + dialogAdsConsentBinding.adsConsentPersonalised.setOnClickListener { + presenter.onPersonalizedAgree() + dialog.dismiss() + } + + dialogAdsConsentBinding.adsConsentNonPersonalised.setOnClickListener { + presenter.onNonPersonalizedAgree() + dialog.dismiss() + } + + dialogAdsConsentBinding.adsConsentPrivacy.setOnClickListener { presenter.onPrivacySelected() } + dialogAdsConsentBinding.adsConsentCancel.setOnClickListener { dialog.cancel() } + } + + override fun showProcessingDataSummary(isPersonalized: Boolean?) { + val summaryText = isPersonalized?.let { + getString(if (it) R.string.pref_ads_summary_personalized else R.string.pref_ads_summary_non_personalized) + } + + findPreference(getString(R.string.pref_key_ads_consent_data_processing)) + ?.summary = summaryText + } + + override fun setCheckedProcessingData(checked: Boolean) { + findPreference(getString(R.string.pref_key_ads_consent_data_processing)) + ?.isChecked = checked + } + + override fun setCheckedAdsEnabled(checked: Boolean) { + findPreference(getString(R.string.pref_key_ads_enabled)) + ?.isChecked = checked } override fun openPrivacyPolicy() { @@ -98,4 +149,4 @@ class AdsFragment : PreferenceFragmentCompat(), MainView.TitledView, AdsView { override fun showErrorDetailsDialog(error: Throwable) { ErrorDialog.newInstance(error).show(childFragmentManager, error.toString()) } -} \ No newline at end of file +} diff --git a/app/src/play/java/io/github/wulkanowy/ui/modules/settings/ads/AdsPresenter.kt b/app/src/play/java/io/github/wulkanowy/ui/modules/settings/ads/AdsPresenter.kt index 5ccbce1e5..772d616d7 100644 --- a/app/src/play/java/io/github/wulkanowy/ui/modules/settings/ads/AdsPresenter.kt +++ b/app/src/play/java/io/github/wulkanowy/ui/modules/settings/ads/AdsPresenter.kt @@ -1,5 +1,6 @@ package io.github.wulkanowy.ui.modules.settings.ads +import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.ErrorHandler @@ -11,24 +12,22 @@ import javax.inject.Inject class AdsPresenter @Inject constructor( errorHandler: ErrorHandler, studentRepository: StudentRepository, - private val adsHelper: AdsHelper + private val adsHelper: AdsHelper, + private val preferencesRepository: PreferencesRepository ) : BasePresenter(errorHandler, studentRepository) { override fun onAttachView(view: AdsView) { super.onAttachView(view) view.initView() Timber.i("Settings ads view was initialized") + + view.showProcessingDataSummary( + preferencesRepository.isPersonalizedAdsEnabled.takeIf { + preferencesRepository.isAgreeToProcessData + }) } fun onWatchSingleAdSelected() { - view?.showPrivacyPolicyDialog() - } - - fun onPrivacySelected() { - view?.openPrivacyPolicy() - } - - fun onAgreedPrivacy() { view?.showLoadingSupportAd(true) presenterScope.launch { runCatching { adsHelper.getSupportAd() } @@ -41,4 +40,39 @@ class AdsPresenter @Inject constructor( } } } -} \ No newline at end of file + + fun onConsentSelected(isChecked: Boolean) { + if (isChecked) { + view?.showPrivacyPolicyDialog() + } else { + view?.showProcessingDataSummary(null) + view?.setCheckedAdsEnabled(false) + } + } + + fun onPrivacySelected() { + view?.openPrivacyPolicy() + } + + fun onPrivacyDialogCanceled() { + view?.setCheckedProcessingData(false) + } + + fun onNonPersonalizedAgree() { + preferencesRepository.isPersonalizedAdsEnabled = false + + adsHelper.initialize() + + view?.setCheckedProcessingData(true) + view?.showProcessingDataSummary(false) + } + + fun onPersonalizedAgree() { + preferencesRepository.isPersonalizedAdsEnabled = true + + adsHelper.initialize() + + view?.setCheckedProcessingData(true) + view?.showProcessingDataSummary(true) + } +} diff --git a/app/src/play/java/io/github/wulkanowy/ui/modules/settings/ads/AdsView.kt b/app/src/play/java/io/github/wulkanowy/ui/modules/settings/ads/AdsView.kt index 89de7bd1d..8de6e60d3 100644 --- a/app/src/play/java/io/github/wulkanowy/ui/modules/settings/ads/AdsView.kt +++ b/app/src/play/java/io/github/wulkanowy/ui/modules/settings/ads/AdsView.kt @@ -16,4 +16,10 @@ interface AdsView : BaseView { fun showLoadingSupportAd(show: Boolean) fun showWatchAdOncePerVisit(show: Boolean) -} \ No newline at end of file + + fun setCheckedAdsEnabled(checked: Boolean) + + fun setCheckedProcessingData(checked: Boolean) + + fun showProcessingDataSummary(isPersonalized: Boolean?) +} diff --git a/app/src/play/java/io/github/wulkanowy/utils/AdsHelper.kt b/app/src/play/java/io/github/wulkanowy/utils/AdsHelper.kt index dde4d0121..c536e2218 100644 --- a/app/src/play/java/io/github/wulkanowy/utils/AdsHelper.kt +++ b/app/src/play/java/io/github/wulkanowy/utils/AdsHelper.kt @@ -2,27 +2,39 @@ package io.github.wulkanowy.utils import android.content.Context import android.os.Bundle +import android.view.View import com.google.ads.mediation.admob.AdMobAdapter -import com.google.android.gms.ads.AdRequest -import com.google.android.gms.ads.LoadAdError -import com.google.android.gms.ads.MobileAds +import com.google.android.gms.ads.* import com.google.android.gms.ads.rewardedinterstitial.RewardedInterstitialAd import com.google.android.gms.ads.rewardedinterstitial.RewardedInterstitialAdLoadCallback import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.BuildConfig +import io.github.wulkanowy.data.repositories.PreferencesRepository import javax.inject.Inject import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException import kotlin.coroutines.suspendCoroutine -class AdsHelper @Inject constructor(@ApplicationContext private val context: Context) { + +class AdsHelper @Inject constructor( + @ApplicationContext private val context: Context, + private val preferencesRepository: PreferencesRepository +) { + + fun initialize() { + if (preferencesRepository.isAgreeToProcessData) { + MobileAds.initialize(context) + } + } suspend fun getSupportAd(): RewardedInterstitialAd? { - MobileAds.initialize(context) - val extra = Bundle().apply { putString("npa", "1") } val adRequest = AdRequest.Builder() - .addNetworkExtrasBundle(AdMobAdapter::class.java, extra) + .apply { + if (!preferencesRepository.isPersonalizedAdsEnabled) { + addNetworkExtrasBundle(AdMobAdapter::class.java, extra) + } + } .build() return suspendCoroutine { @@ -41,4 +53,35 @@ class AdsHelper @Inject constructor(@ApplicationContext private val context: Con }) } } -} \ No newline at end of file + + suspend fun getDashboardTileAdBanner(width: Int): AdBanner { + val extra = Bundle().apply { putString("npa", "1") } + val adRequest = AdRequest.Builder() + .apply { + if (!preferencesRepository.isPersonalizedAdsEnabled) { + addNetworkExtrasBundle(AdMobAdapter::class.java, extra) + } + } + .build() + + return suspendCoroutine { + val adView = AdView(context).apply { + setAdSize(AdSize.getPortraitAnchoredAdaptiveBannerAdSize(context, width)) + adUnitId = BuildConfig.DASHBOARD_TILE_AD_ID + adListener = object : AdListener() { + override fun onAdFailedToLoad(loadAdError: LoadAdError) { + it.resumeWithException(IllegalArgumentException(loadAdError.message)) + } + + override fun onAdLoaded() { + it.resume(AdBanner(this@apply)) + } + } + } + + adView.loadAd(adRequest) + } + } +} + +data class AdBanner(val view: View) diff --git a/app/src/play/res/xml/scheme_preferences_ads.xml b/app/src/play/res/xml/scheme_preferences_ads.xml index 52a3df58d..4165561a7 100644 --- a/app/src/play/res/xml/scheme_preferences_ads.xml +++ b/app/src/play/res/xml/scheme_preferences_ads.xml @@ -2,11 +2,34 @@ + app:title="@string/pref_ads_agreements"> + + + + + - \ No newline at end of file + diff --git a/app/src/release/agconnect-services.json.gpg b/app/src/release/agconnect-services.json.gpg index 484d8bc72..149b9b268 100644 Binary files a/app/src/release/agconnect-services.json.gpg and b/app/src/release/agconnect-services.json.gpg differ diff --git a/app/src/test/java/io/github/wulkanowy/TestEnityCreator.kt b/app/src/test/java/io/github/wulkanowy/TestEnityCreator.kt index a0fa209fd..ff0a53135 100644 --- a/app/src/test/java/io/github/wulkanowy/TestEnityCreator.kt +++ b/app/src/test/java/io/github/wulkanowy/TestEnityCreator.kt @@ -1,15 +1,18 @@ package io.github.wulkanowy +import io.github.wulkanowy.data.db.entities.Mailbox +import io.github.wulkanowy.data.db.entities.MailboxType import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.sdk.Sdk import java.time.LocalDate -import java.time.LocalDateTime.now +import java.time.Instant.now import io.github.wulkanowy.sdk.pojo.Semester as SdkSemester fun getSemesterEntity(diaryId: Int = 1, semesterId: Int = 1, start: LocalDate = LocalDate.now(), end: LocalDate = LocalDate.now(), semesterName: Int = 1) = Semester( studentId = 1, diaryId = diaryId, + kindergartenDiaryId = 0, semesterId = semesterId, diaryName = "$semesterId", schoolYear = 1970, @@ -20,8 +23,19 @@ fun getSemesterEntity(diaryId: Int = 1, semesterId: Int = 1, start: LocalDate = end = end ) +fun getMailboxEntity() = Mailbox( + globalKey = "v4", + fullName = "", + userName = "", + userLoginId = 0, + studentName = "", + schoolNameShort = "", + type = MailboxType.UNKNOWN, +) + fun getSemesterPojo(diaryId: Int, semesterId: Int, start: LocalDate, end: LocalDate, semesterName: Int = 1) = SdkSemester( diaryId = diaryId, + kindergartenDiaryId = 0, semesterId = semesterId, diaryName = "$semesterId", schoolYear = 1970, diff --git a/app/src/test/java/io/github/wulkanowy/utils/FlowUtilsKtTest.kt b/app/src/test/java/io/github/wulkanowy/data/ResourceTest.kt similarity index 91% rename from app/src/test/java/io/github/wulkanowy/utils/FlowUtilsKtTest.kt rename to app/src/test/java/io/github/wulkanowy/data/ResourceTest.kt index 375a24038..ea846a57b 100644 --- a/app/src/test/java/io/github/wulkanowy/utils/FlowUtilsKtTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/ResourceTest.kt @@ -1,23 +1,21 @@ -package io.github.wulkanowy.utils +package io.github.wulkanowy.data -import io.mockk.Runs -import io.mockk.coEvery -import io.mockk.coVerifyOrder -import io.mockk.just -import io.mockk.mockk +import io.mockk.* import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.test.TestCoroutineScope +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.advanceTimeBy import org.junit.Test import kotlin.test.assertEquals @OptIn(ExperimentalCoroutinesApi::class) -class FlowUtilsKtTest { +class ResourceTest { - private val testScope = TestCoroutineScope() + private val testScope = TestScope(UnconfinedTestDispatcher()) @Test fun `fetch from two places with same remote data`() { @@ -43,6 +41,7 @@ class FlowUtilsKtTest { // first networkBoundResource( + isResultEmpty = { false }, showSavedOnLoading = false, query = { repo.query() }, fetch = { @@ -57,6 +56,7 @@ class FlowUtilsKtTest { // second networkBoundResource( + isResultEmpty = { false }, showSavedOnLoading = false, query = { repo.query() }, fetch = { @@ -122,6 +122,7 @@ class FlowUtilsKtTest { // first networkBoundResource( + isResultEmpty = { false }, mutex = saveResultMutex, showSavedOnLoading = false, query = { repo.query() }, @@ -140,6 +141,7 @@ class FlowUtilsKtTest { // second networkBoundResource( + isResultEmpty = { false }, mutex = saveResultMutex, showSavedOnLoading = false, query = { repo.query() }, diff --git a/app/src/test/java/io/github/wulkanowy/data/db/migrations/Migration13Test.kt b/app/src/test/java/io/github/wulkanowy/data/db/migrations/Migration13Test.kt index 2350da459..bdfb4137d 100644 --- a/app/src/test/java/io/github/wulkanowy/data/db/migrations/Migration13Test.kt +++ b/app/src/test/java/io/github/wulkanowy/data/db/migrations/Migration13Test.kt @@ -153,23 +153,26 @@ class Migration13Test : AbstractMigrationTest() { private fun getSemesters(db: SupportSQLiteDatabase, query: String): List> { val semesters = mutableListOf>() - val cursor = db.query(query) - if (cursor.moveToFirst()) { - do { - semesters.add(Semester( - studentId = cursor.getInt(1), - diaryId = cursor.getInt(2), - diaryName = cursor.getString(3), - semesterId = cursor.getInt(4), - semesterName = cursor.getInt(5), - classId = cursor.getInt(7), - unitId = cursor.getInt(8), - schoolYear = cursor.getInt(9), - start = Converters().timestampToDate(cursor.getLong(10))!!, - end = Converters().timestampToDate(cursor.getLong(11))!! - ) to (cursor.getInt(6) == 1)) - } while (cursor.moveToNext()) + db.query(query).use { + if (it.moveToFirst()) { + do { + semesters.add(Semester( + studentId = it.getInt(1), + diaryId = it.getInt(2), + kindergartenDiaryId = 0, + diaryName = it.getString(3), + semesterId = it.getInt(4), + semesterName = it.getInt(5), + classId = it.getInt(7), + unitId = it.getInt(8), + schoolYear = it.getInt(9), + start = Converters().timestampToLocalDate(it.getLong(10))!!, + end = Converters().timestampToLocalDate(it.getLong(11))!! + ) to (it.getInt(6) == 1)) + } while (it.moveToNext()) + } } + return semesters.toList() } diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/AttendanceRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/AttendanceRepositoryTest.kt index f3c7fba77..7d22f7265 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/AttendanceRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/AttendanceRepositoryTest.kt @@ -1,20 +1,17 @@ package io.github.wulkanowy.data.repositories +import io.github.wulkanowy.data.dataOrNull import io.github.wulkanowy.data.db.dao.AttendanceDao +import io.github.wulkanowy.data.errorOrNull import io.github.wulkanowy.data.mappers.mapToEntities +import io.github.wulkanowy.data.toFirstResult import io.github.wulkanowy.getSemesterEntity import io.github.wulkanowy.getStudentEntity import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.AutoRefreshHelper -import io.github.wulkanowy.utils.toFirstResult -import io.mockk.MockKAnnotations -import io.mockk.Runs -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.every +import io.mockk.* import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.SpyK -import io.mockk.just import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals @@ -73,8 +70,8 @@ class AttendanceRepositoryTest { val res = runBlocking { attendanceRepository.getAttendance(student, semester, startDate, endDate, true).toFirstResult() } // verify - assertEquals(null, res.error) - assertEquals(2, res.data?.size) + assertEquals(null, res.errorOrNull) + assertEquals(2, res.dataOrNull?.size) coVerify { sdk.getAttendance(startDate, endDate, 1) } coVerify { attendanceDb.loadAll(1, 1, startDate, endDate) } coVerify { attendanceDb.insertAll(match { it.isEmpty() }) } @@ -97,8 +94,8 @@ class AttendanceRepositoryTest { val res = runBlocking { attendanceRepository.getAttendance(student, semester, startDate, endDate, true).toFirstResult() } // verify - assertEquals(null, res.error) - assertEquals(2, res.data?.size) + assertEquals(null, res.errorOrNull) + assertEquals(2, res.dataOrNull?.size) coVerify { sdk.getAttendance(startDate, endDate, 1) } coVerify { attendanceDb.loadAll(1, 1, startDate, endDate) } coVerify { @@ -125,8 +122,8 @@ class AttendanceRepositoryTest { val res = runBlocking { attendanceRepository.getAttendance(student, semester, startDate, endDate, true).toFirstResult() } // verify - assertEquals(null, res.error) - assertEquals(1, res.data?.size) + assertEquals(null, res.errorOrNull) + assertEquals(1, res.dataOrNull?.size) coVerify { sdk.getAttendance(startDate, endDate, 1) } coVerify { attendanceDb.loadAll(1, 1, startDate, endDate) } coVerify { attendanceDb.insertAll(match { it.isEmpty() }) } diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/CompletedLessonsRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/CompletedLessonsRepositoryTest.kt index fa54522a8..c28ea304b 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/CompletedLessonsRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/CompletedLessonsRepositoryTest.kt @@ -1,20 +1,17 @@ package io.github.wulkanowy.data.repositories +import io.github.wulkanowy.data.dataOrNull import io.github.wulkanowy.data.db.dao.CompletedLessonsDao +import io.github.wulkanowy.data.errorOrNull import io.github.wulkanowy.data.mappers.mapToEntities +import io.github.wulkanowy.data.toFirstResult import io.github.wulkanowy.getSemesterEntity import io.github.wulkanowy.getStudentEntity import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.AutoRefreshHelper -import io.github.wulkanowy.utils.toFirstResult -import io.mockk.MockKAnnotations -import io.mockk.Runs -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.every +import io.mockk.* import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.SpyK -import io.mockk.just import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals @@ -73,8 +70,8 @@ class CompletedLessonsRepositoryTest { val res = runBlocking { completedLessonRepository.getCompletedLessons(student, semester, startDate, endDate, true).toFirstResult() } // verify - assertEquals(null, res.error) - assertEquals(2, res.data?.size) + assertEquals(null, res.errorOrNull) + assertEquals(2, res.dataOrNull?.size) coVerify { sdk.getCompletedLessons(startDate, endDate) } coVerify { completedLessonDb.loadAll(1, 1, startDate, endDate) } coVerify { completedLessonDb.insertAll(match { it.isEmpty() }) } @@ -97,8 +94,8 @@ class CompletedLessonsRepositoryTest { val res = runBlocking { completedLessonRepository.getCompletedLessons(student, semester, startDate, endDate, true).toFirstResult() } // verify - assertEquals(null, res.error) - assertEquals(2, res.data?.size) + assertEquals(null, res.errorOrNull) + assertEquals(2, res.dataOrNull?.size) coVerify { sdk.getCompletedLessons(startDate, endDate) } coVerify { completedLessonDb.loadAll(1, 1, startDate, endDate) } coVerify { @@ -125,8 +122,8 @@ class CompletedLessonsRepositoryTest { val res = runBlocking { completedLessonRepository.getCompletedLessons(student, semester, startDate, endDate, true).toFirstResult() } // verify - assertEquals(null, res.error) - assertEquals(1, res.data?.size) + assertEquals(null, res.errorOrNull) + assertEquals(1, res.dataOrNull?.size) coVerify { sdk.getCompletedLessons(startDate, endDate) } coVerify { completedLessonDb.loadAll(1, 1, startDate, endDate) } coVerify { completedLessonDb.insertAll(match { it.isEmpty() }) } diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/ExamRemoteTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/ExamRemoteTest.kt index 8bf4deee3..e3790662e 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/ExamRemoteTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/ExamRemoteTest.kt @@ -1,20 +1,17 @@ package io.github.wulkanowy.data.repositories +import io.github.wulkanowy.data.dataOrNull import io.github.wulkanowy.data.db.dao.ExamDao +import io.github.wulkanowy.data.errorOrNull import io.github.wulkanowy.data.mappers.mapToEntities +import io.github.wulkanowy.data.toFirstResult import io.github.wulkanowy.getSemesterEntity import io.github.wulkanowy.getStudentEntity import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.AutoRefreshHelper -import io.github.wulkanowy.utils.toFirstResult -import io.mockk.MockKAnnotations -import io.mockk.Runs -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.every +import io.mockk.* import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.SpyK -import io.mockk.just import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals @@ -74,8 +71,8 @@ class ExamRemoteTest { val res = runBlocking { examRepository.getExams(student, semester, startDate, endDate, true).toFirstResult() } // verify - assertEquals(null, res.error) - assertEquals(2, res.data?.size) + assertEquals(null, res.errorOrNull) + assertEquals(2, res.dataOrNull?.size) coVerify { sdk.getExams(startDate, realEndDate, 1) } coVerify { examDb.loadAll(1, 1, startDate, realEndDate) } coVerify { examDb.insertAll(match { it.isEmpty() }) } @@ -98,8 +95,8 @@ class ExamRemoteTest { val res = runBlocking { examRepository.getExams(student, semester, startDate, endDate, true).toFirstResult() } // verify - assertEquals(null, res.error) - assertEquals(2, res.data?.size) + assertEquals(null, res.errorOrNull) + assertEquals(2, res.dataOrNull?.size) coVerify { sdk.getExams(startDate, realEndDate, 1) } coVerify { examDb.loadAll(1, 1, startDate, realEndDate) } coVerify { @@ -126,8 +123,8 @@ class ExamRemoteTest { val res = runBlocking { examRepository.getExams(student, semester, startDate, endDate, true).toFirstResult() } // verify - assertEquals(null, res.error) - assertEquals(1, res.data?.size) + assertEquals(null, res.errorOrNull) + assertEquals(1, res.dataOrNull?.size) coVerify { sdk.getExams(startDate, realEndDate, 1) } coVerify { examDb.loadAll(1, 1, startDate, realEndDate) } coVerify { examDb.insertAll(match { it.isEmpty() }) } diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/GradeRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/GradeRepositoryTest.kt index 6dd30a579..e8d0b6c8f 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/GradeRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/GradeRepositoryTest.kt @@ -1,30 +1,26 @@ package io.github.wulkanowy.data.repositories +import io.github.wulkanowy.data.dataOrNull import io.github.wulkanowy.data.db.dao.GradeDao import io.github.wulkanowy.data.db.dao.GradeSummaryDao +import io.github.wulkanowy.data.errorOrNull import io.github.wulkanowy.data.mappers.mapToEntities +import io.github.wulkanowy.data.toFirstResult import io.github.wulkanowy.getSemesterEntity import io.github.wulkanowy.getStudentEntity import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.AutoRefreshHelper -import io.github.wulkanowy.utils.toFirstResult -import io.mockk.MockKAnnotations -import io.mockk.Runs -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.every +import io.mockk.* import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.SpyK -import io.mockk.just import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.runBlocking -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue +import org.junit.Assert.* import org.junit.Before import org.junit.Test import java.time.LocalDate import java.time.LocalDate.of +import java.time.ZoneOffset import io.github.wulkanowy.sdk.pojo.Grade as SdkGrade class GradeRepositoryTest { @@ -57,7 +53,11 @@ class GradeRepositoryTest { coEvery { gradeDb.deleteAll(any()) } just Runs coEvery { gradeDb.insertAll(any()) } returns listOf() - coEvery { gradeSummaryDb.loadAll(1, 1) } returnsMany listOf(flowOf(listOf()), flowOf(listOf()), flowOf(listOf())) + coEvery { gradeSummaryDb.loadAll(1, 1) } returnsMany listOf( + flowOf(listOf()), + flowOf(listOf()), + flowOf(listOf()) + ) coEvery { gradeSummaryDb.deleteAll(any()) } just Runs coEvery { gradeSummaryDb.insertAll(any()) } returns listOf() } @@ -65,7 +65,7 @@ class GradeRepositoryTest { @Test fun `mark grades older than registration date as read`() { // prepare - val boundaryDate = of(2019, 2, 27).atStartOfDay() + val boundaryDate = of(2019, 2, 27).atStartOfDay().toInstant(ZoneOffset.UTC) val remoteList = listOf( createGradeApi(5, 4.0, of(2019, 2, 25), "Ocena pojawiła się"), createGradeApi(5, 4.0, of(2019, 2, 26), "przed zalogowanie w aplikacji"), @@ -81,11 +81,17 @@ class GradeRepositoryTest { ) // execute - val res = runBlocking { gradeRepository.getGrades(student.copy(registrationDate = boundaryDate), semester, true).toFirstResult() } + val res = runBlocking { + gradeRepository.getGrades( + student = student.copy(registrationDate = boundaryDate), + semester = semester, + forceRefresh = true + ).toFirstResult() + } // verify - assertEquals(null, res.error) - assertEquals(4, res.data?.first?.size) + assertEquals(null, res.errorOrNull) + assertEquals(4, res.dataOrNull?.first?.size) coVerify { gradeDb.insertAll(withArg { assertEquals(4, it.size) @@ -101,9 +107,19 @@ class GradeRepositoryTest { fun `mitigate mark grades as unread when old grades changed`() { // prepare val remoteList = listOf( - createGradeApi(5, 2.0, of(2019, 2, 25), "Ocena ma datę, jest inna, ale nie zostanie powiadomiona"), + createGradeApi( + 5, + 2.0, + of(2019, 2, 25), + "Ocena ma datę, jest inna, ale nie zostanie powiadomiona" + ), createGradeApi(4, 3.0, of(2019, 2, 26), "starszą niż ostatnia lokalnie"), - createGradeApi(3, 4.0, of(2019, 2, 27), "Ta jest z tego samego dnia co ostatnia lokalnie"), + createGradeApi( + 3, + 4.0, + of(2019, 2, 27), + "Ta jest z tego samego dnia co ostatnia lokalnie" + ), createGradeApi(2, 5.0, of(2019, 2, 28), "Ta jest już w ogóle nowa") ) coEvery { sdk.getGrades(1) } returns (remoteList to emptyList()) @@ -111,7 +127,12 @@ class GradeRepositoryTest { val localList = listOf( createGradeApi(5, 3.0, of(2019, 2, 25), "Jedna ocena"), createGradeApi(4, 4.0, of(2019, 2, 26), "Druga"), - createGradeApi(3, 4.0, of(2019, 2, 27), "Ta jest z tego samego dnia co ostatnia lokalnie") + createGradeApi( + 3, + 4.0, + of(2019, 2, 27), + "Ta jest z tego samego dnia co ostatnia lokalnie" + ) ) coEvery { gradeDb.loadAll(1, 1) } returnsMany listOf( flowOf(localList.mapToEntities(semester)), @@ -123,8 +144,8 @@ class GradeRepositoryTest { val res = runBlocking { gradeRepository.getGrades(student, semester, true).toFirstResult() } // verify - assertEquals(null, res.error) - assertEquals(4, res.data?.first?.size) + assertEquals(null, res.errorOrNull) + assertEquals(4, res.dataOrNull?.first?.size) coVerify { gradeDb.insertAll(withArg { assertEquals(3, it.size) @@ -165,8 +186,8 @@ class GradeRepositoryTest { val res = runBlocking { gradeRepository.getGrades(student, semester, true).toFirstResult() } // verify - assertEquals(null, res.error) - assertEquals(2, res.data?.first?.size) + assertEquals(null, res.errorOrNull) + assertEquals(2, res.dataOrNull?.first?.size) coVerify { gradeDb.insertAll(match { it.isEmpty() }) } coVerify { gradeDb.deleteAll(match { it.size == 1 }) } // ... here } @@ -195,8 +216,8 @@ class GradeRepositoryTest { val res = runBlocking { gradeRepository.getGrades(student, semester, true).toFirstResult() } // verify - assertEquals(null, res.error) - assertEquals(3, res.data?.first?.size) + assertEquals(null, res.errorOrNull) + assertEquals(3, res.dataOrNull?.first?.size) coVerify { gradeDb.insertAll(match { it.size == 1 }) } // ... here coVerify { gradeDb.deleteAll(match { it.isEmpty() }) } } @@ -221,8 +242,8 @@ class GradeRepositoryTest { val res = runBlocking { gradeRepository.getGrades(student, semester, true).toFirstResult() } // verify - assertEquals(null, res.error) - assertEquals(3, res.data?.first?.size) + assertEquals(null, res.errorOrNull) + assertEquals(3, res.dataOrNull?.first?.size) } @Test @@ -244,22 +265,23 @@ class GradeRepositoryTest { val res = runBlocking { gradeRepository.getGrades(student, semester, true).toFirstResult() } // verify - assertEquals(null, res.error) - assertEquals(0, res.data?.first?.size) + assertEquals(null, res.errorOrNull) + assertEquals(0, res.dataOrNull?.first?.size) } - private fun createGradeApi(value: Int, weight: Double, date: LocalDate, desc: String) = SdkGrade( - subject = "", - color = "", - comment = "", - date = date, - description = desc, - entry = "", - modifier = .0, - symbol = "", - teacher = "", - value = value.toDouble(), - weight = weight.toString(), - weightValue = weight - ) + private fun createGradeApi(value: Int, weight: Double, date: LocalDate, desc: String) = + SdkGrade( + subject = "", + color = "", + comment = "", + date = date, + description = desc, + entry = "", + modifier = .0, + symbol = "", + teacher = "", + value = value.toDouble(), + weight = weight.toString(), + weightValue = weight + ) } diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/GradeStatisticsRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/GradeStatisticsRepositoryTest.kt index cce3794de..8e2f7c6ef 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/GradeStatisticsRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/GradeStatisticsRepositoryTest.kt @@ -1,24 +1,21 @@ package io.github.wulkanowy.data.repositories +import io.github.wulkanowy.data.dataOrNull import io.github.wulkanowy.data.db.dao.GradePartialStatisticsDao import io.github.wulkanowy.data.db.dao.GradePointsStatisticsDao import io.github.wulkanowy.data.db.dao.GradeSemesterStatisticsDao +import io.github.wulkanowy.data.errorOrNull import io.github.wulkanowy.data.mappers.mapToEntities +import io.github.wulkanowy.data.toFirstResult import io.github.wulkanowy.getSemesterEntity import io.github.wulkanowy.getStudentEntity import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.pojo.GradeStatisticsItem import io.github.wulkanowy.sdk.pojo.GradeStatisticsSubject import io.github.wulkanowy.utils.AutoRefreshHelper -import io.github.wulkanowy.utils.toFirstResult -import io.mockk.MockKAnnotations -import io.mockk.Runs -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.every +import io.mockk.* import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.SpyK -import io.mockk.just import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals @@ -48,22 +45,27 @@ class GradeStatisticsRepositoryTest { private lateinit var gradeStatisticsRepository: GradeStatisticsRepository - private val remotePartialList = listOf( - getGradeStatisticsPartialSubject("Fizyka"), - getGradeStatisticsPartialSubject("Matematyka") - ) - @Before fun setUp() { MockKAnnotations.init(this) every { refreshHelper.shouldBeRefreshed(any()) } returns false - gradeStatisticsRepository = GradeStatisticsRepository(gradePartialStatisticsDb, gradePointsStatisticsDb, gradeSemesterStatisticsDb, sdk, refreshHelper) + gradeStatisticsRepository = GradeStatisticsRepository( + gradePartialStatisticsDb = gradePartialStatisticsDb, + gradePointsStatisticsDb = gradePointsStatisticsDb, + gradeSemesterStatisticsDb = gradeSemesterStatisticsDb, + sdk = sdk, + refreshHelper = refreshHelper, + ) } @Test fun `force refresh without difference`() { // prepare + val remotePartialList = listOf( + getGradeStatisticsPartialSubject("Fizyka"), + getGradeStatisticsPartialSubject("Matematyka") + ) coEvery { sdk.getGradesPartialStatistics(1) } returns remotePartialList coEvery { gradePartialStatisticsDb.loadAll(1, 1) } returnsMany listOf( flowOf(remotePartialList.mapToEntities(semester)), @@ -73,21 +75,74 @@ class GradeStatisticsRepositoryTest { coEvery { gradePartialStatisticsDb.deleteAll(any()) } just Runs // execute - val res = runBlocking { gradeStatisticsRepository.getGradesPartialStatistics(student, semester, "Wszystkie", true).toFirstResult() } + val res = runBlocking { + gradeStatisticsRepository.getGradesPartialStatistics( + student = student, + semester = semester, + subjectName = "Wszystkie", + forceRefresh = true, + ).toFirstResult() + } + val items = res.dataOrNull.orEmpty() // verify - assertEquals(null, res.error) - assertEquals(2 + 1, res.data?.size) + assertEquals(null, res.errorOrNull) + assertEquals(2 + 1, res.dataOrNull?.size) + assertEquals("", items[0].partial?.studentAverage) + assertEquals("", items[1].partial?.studentAverage) + assertEquals("", items[2].partial?.studentAverage) coVerify { sdk.getGradesPartialStatistics(1) } coVerify { gradePartialStatisticsDb.loadAll(1, 1) } coVerify { gradePartialStatisticsDb.insertAll(match { it.isEmpty() }) } coVerify { gradePartialStatisticsDb.deleteAll(match { it.isEmpty() }) } } - private fun getGradeStatisticsPartialSubject(subjectName: String) = GradeStatisticsSubject( + @Test + fun `force refresh without difference with filled up items`() { + // prepare + val remotePartialList = listOf( + getGradeStatisticsPartialSubject("Fizyka", "1.0"), + getGradeStatisticsPartialSubject("Matematyka", "5.0") + ) + coEvery { sdk.getGradesPartialStatistics(1) } returns remotePartialList + coEvery { gradePartialStatisticsDb.loadAll(1, 1) } returnsMany listOf( + flowOf(remotePartialList.mapToEntities(semester)), + flowOf(remotePartialList.mapToEntities(semester)) + ) + coEvery { gradePartialStatisticsDb.insertAll(any()) } returns listOf(1, 2, 3) + coEvery { gradePartialStatisticsDb.deleteAll(any()) } just Runs + + // execute + val res = runBlocking { + gradeStatisticsRepository.getGradesPartialStatistics( + student = student, + semester = semester, + subjectName = "Wszystkie", + forceRefresh = true, + ).toFirstResult() + } + val items = res.dataOrNull.orEmpty() + + // verify + assertEquals(null, res.errorOrNull) + assertEquals(2 + 1, res.dataOrNull?.size) + assertEquals("3,00", items[0].partial?.studentAverage) + assertEquals("1.0", items[1].partial?.studentAverage) + assertEquals("5.0", items[2].partial?.studentAverage) + coVerify { sdk.getGradesPartialStatistics(1) } + coVerify { gradePartialStatisticsDb.loadAll(1, 1) } + coVerify { gradePartialStatisticsDb.insertAll(match { it.isEmpty() }) } + coVerify { gradePartialStatisticsDb.deleteAll(match { it.isEmpty() }) } + } + + private fun getGradeStatisticsPartialSubject( + subjectName: String, + studentAverage: String = "", + classAverage: String = "", + ) = GradeStatisticsSubject( subject = subjectName, - studentAverage = "", - classAverage = "", + studentAverage = studentAverage, + classAverage = classAverage, classItems = listOf( GradeStatisticsItem( subject = subjectName, diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/LuckyNumberRemoteTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/LuckyNumberRemoteTest.kt index a89aad35c..3225c3bd2 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/LuckyNumberRemoteTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/LuckyNumberRemoteTest.kt @@ -1,17 +1,15 @@ package io.github.wulkanowy.data.repositories +import io.github.wulkanowy.data.dataOrNull import io.github.wulkanowy.data.db.dao.LuckyNumberDao +import io.github.wulkanowy.data.errorOrNull import io.github.wulkanowy.data.mappers.mapToEntity +import io.github.wulkanowy.data.toFirstResult import io.github.wulkanowy.getStudentEntity import io.github.wulkanowy.sdk.Sdk -import io.github.wulkanowy.utils.toFirstResult -import io.mockk.MockKAnnotations -import io.mockk.Runs -import io.mockk.coEvery -import io.mockk.coVerify +import io.mockk.* import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.SpyK -import io.mockk.just import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals @@ -58,8 +56,8 @@ class LuckyNumberRemoteTest { val res = runBlocking { luckyNumberRepository.getLuckyNumber(student, true).toFirstResult() } // verify - assertEquals(null, res.error) - assertEquals(luckyNumber.number, res.data?.luckyNumber) + assertEquals(null, res.errorOrNull) + assertEquals(luckyNumber.number, res.dataOrNull?.luckyNumber) coVerify { sdk.getLuckyNumber(student.schoolShortName) } coVerify { luckyNumberDb.load(1, date) } coVerify(exactly = 0) { luckyNumberDb.insertAll(any()) } @@ -82,8 +80,8 @@ class LuckyNumberRemoteTest { val res = runBlocking { luckyNumberRepository.getLuckyNumber(student, true).toFirstResult() } // verify - assertEquals(null, res.error) - assertEquals(luckyNumber.number, res.data?.luckyNumber) + assertEquals(null, res.errorOrNull) + assertEquals(luckyNumber.number, res.dataOrNull?.luckyNumber) coVerify { sdk.getLuckyNumber(student.schoolShortName) } coVerify { luckyNumberDb.load(1, date) } coVerify { @@ -112,8 +110,8 @@ class LuckyNumberRemoteTest { val res = runBlocking { luckyNumberRepository.getLuckyNumber(student, true).toFirstResult() } // verify - assertEquals(null, res.error) - assertEquals(luckyNumber.number, res.data?.luckyNumber) + assertEquals(null, res.errorOrNull) + assertEquals(luckyNumber.number, res.dataOrNull?.luckyNumber) coVerify { sdk.getLuckyNumber(student.schoolShortName) } coVerify { luckyNumberDb.load(1, date) } coVerify { diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/MailboxRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/MailboxRepositoryTest.kt new file mode 100644 index 000000000..9198560fe --- /dev/null +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/MailboxRepositoryTest.kt @@ -0,0 +1,202 @@ +package io.github.wulkanowy.data.repositories + +import io.github.wulkanowy.data.db.dao.MailboxDao +import io.github.wulkanowy.data.db.entities.Mailbox +import io.github.wulkanowy.data.db.entities.MailboxType +import io.github.wulkanowy.data.db.entities.Student +import io.github.wulkanowy.sdk.Sdk +import io.github.wulkanowy.utils.AutoRefreshHelper +import io.mockk.MockKAnnotations +import io.mockk.Runs +import io.mockk.coEvery +import io.mockk.impl.annotations.MockK +import io.mockk.impl.annotations.SpyK +import io.mockk.just +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import java.time.Instant + +@OptIn(ExperimentalCoroutinesApi::class) +class MailboxRepositoryTest { + + @SpyK + private var sdk = Sdk() + + @MockK + private lateinit var mailboxDao: MailboxDao + + @MockK + private lateinit var refreshHelper: AutoRefreshHelper + + private lateinit var systemUnderTest: MailboxRepository + + @Before + fun setUp() { + MockKAnnotations.init(this) + + coEvery { refreshHelper.shouldBeRefreshed(any()) } returns false + coEvery { refreshHelper.updateLastRefreshTimestamp(any()) } just Runs + coEvery { mailboxDao.deleteAll(any()) } just Runs + coEvery { mailboxDao.insertAll(any()) } returns emptyList() + coEvery { mailboxDao.loadAll(any()) } returns emptyList() + coEvery { sdk.getMailboxes() } returns emptyList() + + systemUnderTest = MailboxRepository( + mailboxDao = mailboxDao, + sdk = sdk, + refreshHelper = refreshHelper, + ) + } + + @Test(expected = IllegalArgumentException::class) + fun `get mailbox that doesn't exist`() = runTest { + val student = getStudentEntity( + userName = "Stanisław Kowalski", + studentName = "Jan Kowalski", + ) + coEvery { sdk.getMailboxes() } returns emptyList() + + systemUnderTest.getMailbox(student) + } + + @Test + fun `get mailbox for user with additional spaces`() = runTest { + val student = getStudentEntity( + userName = " Stanisław Kowalski ", + studentName = " Jan Kowalski ", + ) + val expectedMailbox = getMailboxEntity("Jan Kowalski ") + coEvery { mailboxDao.loadAll(any()) } returns listOf( + expectedMailbox, + ) + + val selectedMailbox = systemUnderTest.getMailbox(student) + assertEquals(expectedMailbox, selectedMailbox) + } + + @Test + fun `get mailbox for unique non-authorized student`() = runTest { + val student = getStudentEntity( + userName = "Stanisław Kowalski", + studentName = "J** K*******", + ) + val expectedMailbox = getMailboxEntity("Jan Kowalski") + coEvery { mailboxDao.loadAll(any()) } returns listOf( + expectedMailbox, + ) + + assertEquals(expectedMailbox, systemUnderTest.getMailbox(student)) + } + + @Test + fun `get mailbox for unique non-authorized student but with spaces`() = runTest { + val student = getStudentEntity( + userName = "Stanisław Kowalski", + studentName = "J** K*******", + ) + val expectedMailbox = getMailboxEntity("Jan Kowalski") + coEvery { mailboxDao.loadAll(any()) } returns listOf( + expectedMailbox, + ) + + assertEquals(expectedMailbox, systemUnderTest.getMailbox(student)) + } + + @Test(expected = IllegalArgumentException::class) + fun `get mailbox for not-unique non-authorized student`() = runTest { + val student = getStudentEntity( + userName = "Stanisław Kowalski", + studentName = "J** K*******", + ) + coEvery { mailboxDao.loadAll(any()) } returns listOf( + getMailboxEntity("Jan Kowalski"), + getMailboxEntity("Jan Kurowski"), + ) + + systemUnderTest.getMailbox(student) + } + + @Test + fun `get mailbox for student with uppercase name`() = runTest { + val student = getStudentEntity( + userName = "Mochoń Julia", + studentName = "KLAUDIA MOCHOŃ", + ) + val expectedMailbox = getMailboxEntity("Klaudia Mochoń") + coEvery { mailboxDao.loadAll(any()) } returns listOf( + expectedMailbox, + ) + + assertEquals(expectedMailbox, systemUnderTest.getMailbox(student)) + } + + @Test + fun `get mailbox for student with second name`() = runTest { + val student = getStudentEntity( + userName = "Fistaszek Karolina", + studentName = "Julia Fistaszek", + ) + val expectedMailbox = getMailboxEntity("Julia Maria Fistaszek") + coEvery { mailboxDao.loadAll(any()) } returns listOf( + expectedMailbox, + ) + + assertEquals(expectedMailbox, systemUnderTest.getMailbox(student)) + } + + @Test + fun `get mailbox for student with second name and uppercase`() = runTest { + val student = getStudentEntity( + userName = "BEDNAREK KAMIL", + studentName = "ALEKSANDRA BEDNAREK", + ) + val expectedMailbox = getMailboxEntity("Aleksandra Anna Bednarek") + coEvery { mailboxDao.loadAll(any()) } returns listOf( + expectedMailbox, + ) + + assertEquals(expectedMailbox, systemUnderTest.getMailbox(student)) + } + + private fun getMailboxEntity( + studentName: String, + ) = Mailbox( + globalKey = "", + fullName = "", + userName = "", + userLoginId = 123, + studentName = studentName, + schoolNameShort = "", + type = MailboxType.STUDENT, + ) + + private fun getStudentEntity( + studentName: String, + userName: String, + ) = Student( + scrapperBaseUrl = "http://fakelog.cf", + email = "jan@fakelog.cf", + certificateKey = "", + classId = 0, + className = "", + isCurrent = false, + isParent = false, + loginMode = Sdk.Mode.API.name, + loginType = Sdk.ScrapperLoginType.STANDARD.name, + mobileBaseUrl = "", + password = "", + privateKey = "", + registrationDate = Instant.now(), + schoolName = "", + schoolShortName = "test", + schoolSymbol = "", + studentId = 1, + studentName = studentName, + symbol = "", + userLoginId = 1, + userName = userName, + ) +} diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/MessageRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/MessageRepositoryTest.kt index 052f08f00..4efc9c60a 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/MessageRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/MessageRepositoryTest.kt @@ -1,42 +1,40 @@ package io.github.wulkanowy.data.repositories import android.content.Context -import io.github.wulkanowy.data.Status +import io.github.wulkanowy.data.dataOrNull import io.github.wulkanowy.data.db.SharedPrefProvider import io.github.wulkanowy.data.db.dao.MessageAttachmentDao import io.github.wulkanowy.data.db.dao.MessagesDao import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.db.entities.MessageWithAttachment import io.github.wulkanowy.data.enums.MessageFolder -import io.github.wulkanowy.getSemesterEntity +import io.github.wulkanowy.data.errorOrNull +import io.github.wulkanowy.data.toFirstResult +import io.github.wulkanowy.getMailboxEntity import io.github.wulkanowy.getStudentEntity import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.pojo.Folder -import io.github.wulkanowy.sdk.pojo.MessageDetails -import io.github.wulkanowy.sdk.pojo.Sender import io.github.wulkanowy.utils.AutoRefreshHelper -import io.github.wulkanowy.utils.toFirstResult -import io.mockk.MockKAnnotations -import io.mockk.Runs -import io.mockk.checkEquals -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.every +import io.github.wulkanowy.utils.Status +import io.github.wulkanowy.utils.status +import io.mockk.* import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.SpyK -import io.mockk.just -import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.toList import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest import kotlinx.serialization.json.Json import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test import java.net.UnknownHostException -import java.time.LocalDateTime +import java.time.Instant +import java.time.ZoneOffset import kotlin.test.assertTrue +@OptIn(ExperimentalCoroutinesApi::class) class MessageRepositoryTest { @SpyK @@ -59,7 +57,7 @@ class MessageRepositoryTest { private val student = getStudentEntity() - private val semester = getSemesterEntity() + private val mailbox = getMailboxEntity() private lateinit var repository: MessageRepository @@ -79,63 +77,22 @@ class MessageRepositoryTest { ) } - @Test - fun `get messages when read by values was changed on already read message`() = runBlocking { - every { messageDb.loadAll(any(), any()) } returns flow { - val dbMessage = getMessageEntity(3, "", false).apply { - unreadBy = 10 - readBy = 5 - isNotified = true - } - emit(listOf(dbMessage)) - } - coEvery { sdk.getMessages(Folder.RECEIVED, any(), any()) } returns listOf( - getMessageDto(messageId = 3, content = "", unread = false).copy( - unreadBy = 5, - readBy = 10, - ) - ) - coEvery { messageDb.deleteAll(any()) } just Runs - coEvery { messageDb.insertAll(any()) } returns listOf() - - repository.getMessages( - student = student, - semester = semester, - folder = MessageFolder.RECEIVED, - forceRefresh = true, - notify = true, // all new messages will be marked as not notified - ).toFirstResult().data.orEmpty() - - coVerify(exactly = 1) { messageDb.deleteAll(emptyList()) } - coVerify(exactly = 1) { messageDb.insertAll(emptyList()) } - coVerify(exactly = 1) { - messageDb.updateAll(withArg { - assertEquals(1, it.size) - assertEquals(5, it.single().unreadBy) - assertEquals(10, it.single().readBy) - }) - } - } - @Test fun `get messages when fetched completely new message without notify`() = runBlocking { every { messageDb.loadAll(any(), any()) } returns flowOf(emptyList()) - coEvery { sdk.getMessages(Folder.RECEIVED, any(), any()) } returns listOf( - getMessageDto(messageId = 4, content = "Test", unread = true).copy( - unreadBy = 5, - readBy = 10, - ) + coEvery { sdk.getMessages(Folder.RECEIVED, any()) } returns listOf( + getMessageDto() ) coEvery { messageDb.deleteAll(any()) } just Runs coEvery { messageDb.insertAll(any()) } returns listOf() repository.getMessages( student = student, - semester = semester, + mailbox = mailbox, folder = MessageFolder.RECEIVED, forceRefresh = true, notify = false, - ).toFirstResult().data.orEmpty() + ).toFirstResult().dataOrNull.orEmpty() coVerify(exactly = 1) { messageDb.deleteAll(withArg { checkEquals(emptyList()) }) } coVerify { @@ -150,7 +107,7 @@ class MessageRepositoryTest { fun `throw error when message is not in the db`() { val testMessage = getMessageEntity(1, "", false) coEvery { - messageDb.loadMessageWithAttachment(1, 1) + messageDb.loadMessageWithAttachment("v4") } throws NoSuchElementException("No message in database") runBlocking { repository.getMessage(student, testMessage).toFirstResult() } @@ -161,19 +118,19 @@ class MessageRepositoryTest { val testMessage = getMessageEntity(123, "Test", false) val messageWithAttachment = MessageWithAttachment(testMessage, emptyList()) - coEvery { messageDb.loadMessageWithAttachment(1, testMessage.messageId) } returns flowOf( + coEvery { messageDb.loadMessageWithAttachment("v4") } returns flowOf( messageWithAttachment ) val res = runBlocking { repository.getMessage(student, testMessage).toFirstResult() } - assertEquals(null, res.error) + assertEquals(null, res.errorOrNull) assertEquals(Status.SUCCESS, res.status) - assertEquals("Test", res.data!!.message.content) + assertEquals("Test", res.dataOrNull!!.message.content) } @Test - fun `get message when content in db is empty`() { + fun `get message when content in db is empty`() = runTest { val testMessage = getMessageEntity(123, "", true) val testMessageWithContent = testMessage.copy().apply { content = "Test" } @@ -181,27 +138,23 @@ class MessageRepositoryTest { val mWaWithContent = MessageWithAttachment(testMessageWithContent, emptyList()) coEvery { - messageDb.loadMessageWithAttachment( - 1, - testMessage.messageId - ) + messageDb.loadMessageWithAttachment("v4") } returnsMany listOf(flowOf(mWa), flowOf(mWaWithContent)) coEvery { - sdk.getMessageDetails( - messageId = testMessage.messageId, - folderId = 1, - read = false, - id = testMessage.realId - ) - } returns MessageDetails("Test", emptyList()) + sdk.getMessageDetails("v4", any()) + } returns mockk { + every { sender } returns "" + every { recipients } returns listOf("") + every { attachments } returns listOf() + } coEvery { messageDb.updateAll(any()) } just Runs coEvery { messageAttachmentDao.insertAttachments(any()) } returns listOf(1) - val res = runBlocking { repository.getMessage(student, testMessage).toFirstResult() } + val res = repository.getMessage(student, testMessage).toFirstResult() - assertEquals(null, res.error) + assertEquals(null, res.errorOrNull) assertEquals(Status.SUCCESS, res.status) - assertEquals("Test", res.data!!.message.content) + assertEquals("Test", res.dataOrNull!!.message.content) coVerify { messageDb.updateAll(listOf(testMessageWithContent)) } } @@ -210,7 +163,7 @@ class MessageRepositoryTest { val testMessage = getMessageEntity(123, "", false) coEvery { - messageDb.loadMessageWithAttachment(1, testMessage.messageId) + messageDb.loadMessageWithAttachment("v4") } throws UnknownHostException() runBlocking { repository.getMessage(student, testMessage).toFirstResult() } @@ -221,7 +174,7 @@ class MessageRepositoryTest { val testMessage = getMessageEntity(123, "", true) coEvery { - messageDb.loadMessageWithAttachment(1, testMessage.messageId) + messageDb.loadMessageWithAttachment("v4") } throws UnknownHostException() runBlocking { repository.getMessage(student, testMessage).toList()[1] } @@ -232,41 +185,30 @@ class MessageRepositoryTest { content: String, unread: Boolean ) = Message( - studentId = 1, - realId = 1, + messageGlobalKey = "v4", + mailboxKey = "", + correspondents = "", messageId = messageId, - sender = "", - senderId = 0, - recipient = "Wielu adresatów", subject = "", - date = LocalDateTime.MAX, + date = Instant.EPOCH, folderId = 1, unread = unread, - removed = false, hasAttachments = false ).apply { this.content = content - unreadBy = 1 - readBy = 1 } - private fun getMessageDto( - messageId: Int, - content: String, - unread: Boolean, - ) = io.github.wulkanowy.sdk.pojo.Message( - id = 1, - messageId = messageId, - sender = Sender("", "", 0, 0, 0, ""), + private fun getMessageDto() = io.github.wulkanowy.sdk.pojo.Message( + globalKey = "v4", + mailbox = "", + correspondents = "", + id = 4, recipients = listOf(), subject = "", - content = content, - date = LocalDateTime.MAX, + content = "Test", + dateZoned = Instant.EPOCH.atZone(ZoneOffset.UTC), folderId = 1, - unread = unread, - unreadBy = 0, - readBy = 0, - removed = false, + unread = true, hasAttachments = false, ) } diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/MobileDeviceRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/MobileDeviceRepositoryTest.kt index 52a076d3c..6865aa7da 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/MobileDeviceRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/MobileDeviceRepositoryTest.kt @@ -1,27 +1,25 @@ package io.github.wulkanowy.data.repositories +import io.github.wulkanowy.data.dataOrNull import io.github.wulkanowy.data.db.dao.MobileDeviceDao +import io.github.wulkanowy.data.errorOrNull import io.github.wulkanowy.data.mappers.mapToEntities +import io.github.wulkanowy.data.toFirstResult import io.github.wulkanowy.getSemesterEntity import io.github.wulkanowy.getStudentEntity import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.pojo.Device import io.github.wulkanowy.utils.AutoRefreshHelper -import io.github.wulkanowy.utils.toFirstResult -import io.mockk.MockKAnnotations -import io.mockk.Runs -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.every +import io.mockk.* import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.SpyK -import io.mockk.just import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.runBlocking import org.junit.Assert import org.junit.Before import org.junit.Test import java.time.LocalDateTime.of +import java.time.ZoneId class MobileDeviceRepositoryTest { @@ -58,8 +56,8 @@ class MobileDeviceRepositoryTest { // prepare coEvery { sdk.getRegisteredDevices() } returns remoteList coEvery { mobileDeviceDb.loadAll(student.studentId) } returnsMany listOf( - flowOf(remoteList.mapToEntities(semester)), - flowOf(remoteList.mapToEntities(semester)) + flowOf(remoteList.mapToEntities(student)), + flowOf(remoteList.mapToEntities(student)) ) coEvery { mobileDeviceDb.insertAll(any()) } returns listOf(1, 2, 3) coEvery { mobileDeviceDb.deleteAll(any()) } just Runs @@ -68,8 +66,8 @@ class MobileDeviceRepositoryTest { val res = runBlocking { mobileDeviceRepository.getDevices(student, semester, true).toFirstResult() } // verify - Assert.assertEquals(null, res.error) - Assert.assertEquals(2, res.data?.size) + Assert.assertEquals(null, res.errorOrNull) + Assert.assertEquals(2, res.dataOrNull?.size) coVerify { sdk.getRegisteredDevices() } coVerify { mobileDeviceDb.loadAll(1) } coVerify { mobileDeviceDb.insertAll(match { it.isEmpty() }) } @@ -81,9 +79,9 @@ class MobileDeviceRepositoryTest { // prepare coEvery { sdk.getRegisteredDevices() } returns remoteList coEvery { mobileDeviceDb.loadAll(1) } returnsMany listOf( - flowOf(remoteList.dropLast(1).mapToEntities(semester)), - flowOf(remoteList.dropLast(1).mapToEntities(semester)), // after fetch end before save result - flowOf(remoteList.mapToEntities(semester)) + flowOf(remoteList.dropLast(1).mapToEntities(student)), + flowOf(remoteList.dropLast(1).mapToEntities(student)), // after fetch end before save result + flowOf(remoteList.mapToEntities(student)) ) coEvery { mobileDeviceDb.insertAll(any()) } returns listOf(1, 2, 3) coEvery { mobileDeviceDb.deleteAll(any()) } just Runs @@ -92,13 +90,13 @@ class MobileDeviceRepositoryTest { val res = runBlocking { mobileDeviceRepository.getDevices(student, semester, true).toFirstResult() } // verify - Assert.assertEquals(null, res.error) - Assert.assertEquals(2, res.data?.size) + Assert.assertEquals(null, res.errorOrNull) + Assert.assertEquals(2, res.dataOrNull?.size) coVerify { sdk.getRegisteredDevices() } coVerify { mobileDeviceDb.loadAll(1) } coVerify { mobileDeviceDb.insertAll(match { - it.size == 1 && it[0] == remoteList.mapToEntities(semester)[1] + it.size == 1 && it[0] == remoteList.mapToEntities(student)[1] }) } coVerify { mobileDeviceDb.deleteAll(match { it.isEmpty() }) } @@ -109,9 +107,9 @@ class MobileDeviceRepositoryTest { // prepare coEvery { sdk.getRegisteredDevices() } returns remoteList.dropLast(1) coEvery { mobileDeviceDb.loadAll(1) } returnsMany listOf( - flowOf(remoteList.mapToEntities(semester)), - flowOf(remoteList.mapToEntities(semester)), // after fetch end before save result - flowOf(remoteList.dropLast(1).mapToEntities(semester)) + flowOf(remoteList.mapToEntities(student)), + flowOf(remoteList.mapToEntities(student)), // after fetch end before save result + flowOf(remoteList.dropLast(1).mapToEntities(student)) ) coEvery { mobileDeviceDb.insertAll(any()) } returns listOf(1, 2, 3) coEvery { mobileDeviceDb.deleteAll(any()) } just Runs @@ -120,14 +118,14 @@ class MobileDeviceRepositoryTest { val res = runBlocking { mobileDeviceRepository.getDevices(student, semester, true).toFirstResult() } // verify - Assert.assertEquals(null, res.error) - Assert.assertEquals(1, res.data?.size) + Assert.assertEquals(null, res.errorOrNull) + Assert.assertEquals(1, res.dataOrNull?.size) coVerify { sdk.getRegisteredDevices() } coVerify { mobileDeviceDb.loadAll(1) } coVerify { mobileDeviceDb.insertAll(match { it.isEmpty() }) } coVerify { mobileDeviceDb.deleteAll(match { - it.size == 1 && it[0] == remoteList.mapToEntities(semester)[1] + it.size == 1 && it[0] == remoteList.mapToEntities(student)[1] }) } } @@ -137,6 +135,8 @@ class MobileDeviceRepositoryTest { name = "", deviceId = "", createDate = of(2019, 5, day, 0, 0, 0), - modificationDate = of(2019, 5, day, 0, 0, 0) + modificationDate = of(2019, 5, day, 0, 0, 0), + createDateZoned = of(2019, 5, day, 0, 0, 0).atZone(ZoneId.systemDefault()), + modificationDateZoned = of(2019, 5, day, 0, 0, 0).atZone(ZoneId.systemDefault()) ) } diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/RecipientLocalTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/RecipientLocalTest.kt index 980abac0a..ae73a7958 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/RecipientLocalTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/RecipientLocalTest.kt @@ -1,19 +1,15 @@ package io.github.wulkanowy.data.repositories import io.github.wulkanowy.data.db.dao.RecipientDao -import io.github.wulkanowy.data.db.entities.ReportingUnit import io.github.wulkanowy.data.mappers.mapToEntities +import io.github.wulkanowy.getMailboxEntity import io.github.wulkanowy.getStudentEntity import io.github.wulkanowy.sdk.Sdk +import io.github.wulkanowy.sdk.pojo.MailboxType import io.github.wulkanowy.utils.AutoRefreshHelper -import io.mockk.MockKAnnotations -import io.mockk.Runs -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.every +import io.mockk.* import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.SpyK -import io.mockk.just import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals import org.junit.Before @@ -36,9 +32,30 @@ class RecipientLocalTest { private lateinit var recipientRepository: RecipientRepository private val remoteList = listOf( - SdkRecipient("2rPracownik", "Kowalski Jan", 3, 4, 2, "hash", "Kowalski Jan [KJ] - Pracownik (Fake123456)"), - SdkRecipient("3rPracownik", "Kowalska Karolina", 4, 4, 2, "hash", "Kowalska Karolina [KK] - Pracownik (Fake123456)"), - SdkRecipient("4rPracownik", "Krupa Stanisław", 5, 4, 1, "hash", "Krupa Stanisław [KS] - Uczeń (Fake123456)") + SdkRecipient( + mailboxGlobalKey = "2rPracownik", + userName = "Kowalski Jan", + fullName = "Kowalski Jan [KJ] - Pracownik (Fake123456)", + studentName = "", + schoolNameShort = "", + type = MailboxType.UNKNOWN, + ), + SdkRecipient( + mailboxGlobalKey = "3rPracownik", + userName = "Kowalska Karolina", + fullName = "Kowalska Karolina [KK] - Pracownik (Fake123456)", + studentName = "", + schoolNameShort = "", + type = MailboxType.UNKNOWN, + ), + SdkRecipient( + mailboxGlobalKey = "4rPracownik", + userName = "Krupa Stanisław", + fullName = "Krupa Stanisław [KS] - Uczeń (Fake123456)", + studentName = "", + schoolNameShort = "", + type = MailboxType.UNKNOWN, + ) ) @Before @@ -52,39 +69,61 @@ class RecipientLocalTest { @Test fun `load recipients when items already in database`() { // prepare - coEvery { recipientDb.loadAll(4, 123, 7) } returnsMany listOf( - remoteList.mapToEntities(4), - remoteList.mapToEntities(4) + coEvery { recipientDb.loadAll(io.github.wulkanowy.data.db.entities.MailboxType.UNKNOWN, "v4") } returnsMany listOf( + remoteList.mapToEntities("v4"), + remoteList.mapToEntities("v4") ) coEvery { recipientDb.insertAll(any()) } returns listOf(1, 2, 3) coEvery { recipientDb.deleteAll(any()) } just Runs // execute - val res = runBlocking { recipientRepository.getRecipients(student, ReportingUnit(4, 123, "", 4, "", listOf()), 7) } + val res = runBlocking { + recipientRepository.getRecipients( + student = student, + mailbox = getMailboxEntity(), + type = io.github.wulkanowy.data.db.entities.MailboxType.UNKNOWN, + ) + } // verify assertEquals(3, res.size) - coVerify { recipientDb.loadAll(4, 123, 7) } + coVerify { + recipientDb.loadAll( + type = io.github.wulkanowy.data.db.entities.MailboxType.UNKNOWN, + studentMailboxGlobalKey = "v4" + ) + } } @Test fun `load recipients when database is empty`() { // prepare - coEvery { sdk.getRecipients(123, 7) } returns remoteList - coEvery { recipientDb.loadAll(4, 123, 7) } returnsMany listOf( + coEvery { sdk.getRecipients("v4") } returns remoteList + coEvery { + recipientDb.loadAll( + io.github.wulkanowy.data.db.entities.MailboxType.UNKNOWN, + "v4" + ) + } returnsMany listOf( emptyList(), - remoteList.mapToEntities(4) + remoteList.mapToEntities("v4") ) coEvery { recipientDb.insertAll(any()) } returns listOf(1, 2, 3) coEvery { recipientDb.deleteAll(any()) } just Runs // execute - val res = runBlocking { recipientRepository.getRecipients(student, ReportingUnit(4, 123, "", 4, "", listOf()), 7) } + val res = runBlocking { + recipientRepository.getRecipients( + student = student, + mailbox = getMailboxEntity(), + type = io.github.wulkanowy.data.db.entities.MailboxType.UNKNOWN, + ) + } // verify assertEquals(3, res.size) - coVerify { sdk.getRecipients(123, 7) } - coVerify { recipientDb.loadAll(4, 123, 7) } + coVerify { sdk.getRecipients("v4") } + coVerify { recipientDb.loadAll(io.github.wulkanowy.data.db.entities.MailboxType.UNKNOWN, "v4") } coVerify { recipientDb.insertAll(match { it.isEmpty() }) } coVerify { recipientDb.deleteAll(match { it.isEmpty() }) } } diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/TimetableRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/TimetableRepositoryTest.kt index edb3125eb..e56aaa5d0 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/TimetableRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/TimetableRepositoryTest.kt @@ -1,24 +1,21 @@ package io.github.wulkanowy.data.repositories +import io.github.wulkanowy.data.dataOrNull 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.errorOrNull import io.github.wulkanowy.data.mappers.mapToEntities +import io.github.wulkanowy.data.toFirstResult import io.github.wulkanowy.getSemesterEntity import io.github.wulkanowy.getStudentEntity import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.pojo.TimetableFull import io.github.wulkanowy.services.alarm.TimetableNotificationSchedulerHelper import io.github.wulkanowy.utils.AutoRefreshHelper -import io.github.wulkanowy.utils.toFirstResult -import io.mockk.MockKAnnotations -import io.mockk.Runs -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.every +import io.mockk.* import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.SpyK -import io.mockk.just import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals @@ -27,6 +24,7 @@ import org.junit.Test import java.time.LocalDate import java.time.LocalDateTime import java.time.LocalDateTime.of +import java.time.ZoneId import io.github.wulkanowy.sdk.pojo.Timetable as SdkTimetable class TimetableRepositoryTest { @@ -95,8 +93,8 @@ class TimetableRepositoryTest { val res = runBlocking { timetableRepository.getTimetable(student, semester, startDate, endDate, true).toFirstResult() } // verify - assertEquals(null, res.error) - assertEquals(2, res.data?.lessons?.size) + assertEquals(null, res.errorOrNull) + assertEquals(2, res.dataOrNull!!.lessons.size) coVerify { sdk.getTimetableFull(startDate, endDate) } coVerify { timetableDb.loadAll(1, 1, startDate, endDate) } coVerify { timetableDb.insertAll(match { it.isEmpty() }) } @@ -107,6 +105,8 @@ class TimetableRepositoryTest { number = number, start = start, end = start.plusMinutes(45), + startZoned = start.atZone(ZoneId.systemDefault()), + endZoned = start.plusMinutes(45).atZone(ZoneId.systemDefault()), date = start.toLocalDate(), subject = subject, group = "", diff --git a/app/src/test/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProviderTest.kt b/app/src/test/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProviderTest.kt index ec07e149f..a6ecdc26b 100644 --- a/app/src/test/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProviderTest.kt +++ b/app/src/test/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProviderTest.kt @@ -1,16 +1,18 @@ package io.github.wulkanowy.ui.modules.grade import io.github.wulkanowy.data.Resource -import io.github.wulkanowy.data.Status +import io.github.wulkanowy.data.dataOrNull import io.github.wulkanowy.data.db.entities.Grade import io.github.wulkanowy.data.db.entities.GradeSummary import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.repositories.GradeRepository import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.SemesterRepository +import io.github.wulkanowy.data.resourceFlow import io.github.wulkanowy.getSemesterEntity import io.github.wulkanowy.sdk.Sdk -import io.github.wulkanowy.utils.flowWithResource +import io.github.wulkanowy.utils.Status +import io.github.wulkanowy.utils.status import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.every @@ -23,13 +25,13 @@ import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test +import java.time.Instant import java.time.LocalDate.now import java.time.LocalDate.of -import java.time.LocalDateTime class GradeAverageProviderTest { - private suspend fun Flow>.getResult() = toList()[1].data!! + private suspend fun Flow>.getResult() = toList()[1].dataOrNull!! @MockK lateinit var preferencesRepository: PreferencesRepository @@ -63,7 +65,7 @@ class GradeAverageProviderTest { className = "", classId = 1, isCurrent = true, - registrationDate = LocalDateTime.now() + registrationDate = Instant.now() ) private val semesters = mutableListOf( @@ -144,7 +146,13 @@ class GradeAverageProviderTest { every { preferencesRepository.gradeAverageForceCalc } returns false every { preferencesRepository.isOptionalArithmeticAverage } returns false every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.ONE_SEMESTER - coEvery { gradeRepository.getGrades(student, semesters[2], true) } returns flowWithResource { noWeightGrades to noWeightGradesSummary } + coEvery { + gradeRepository.getGrades( + student, + semesters[2], + true + ) + } returns resourceFlow { noWeightGrades to noWeightGradesSummary } val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId, true).getResult() } @@ -156,7 +164,13 @@ class GradeAverageProviderTest { every { preferencesRepository.gradeAverageForceCalc } returns false every { preferencesRepository.isOptionalArithmeticAverage } returns true every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.ONE_SEMESTER - coEvery { gradeRepository.getGrades(student, semesters[2], true) } returns flowWithResource { noWeightGrades to noWeightGradesArithmeticSummary } + coEvery { + gradeRepository.getGrades( + student, + semesters[2], + true + ) + } returns resourceFlow { noWeightGrades to noWeightGradesArithmeticSummary } val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId, true).getResult() } @@ -170,27 +184,27 @@ class GradeAverageProviderTest { every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.ONE_SEMESTER coEvery { semesterRepository.getSemesters(student) } returns semesters coEvery { gradeRepository.getGrades(student, semesters[2], true) } returns flow { - emit(Resource.loading()) - emit(Resource.loading(secondGradeWithModifier to secondSummariesWithModifier)) - emit(Resource.success(secondGradeWithModifier to secondSummariesWithModifier)) + emit(Resource.Loading()) + emit(Resource.Intermediate(secondGradeWithModifier to secondSummariesWithModifier)) + emit(Resource.Success(secondGradeWithModifier to secondSummariesWithModifier)) } val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId, true).toList() } with(items[0]) { assertEquals(Status.LOADING, status) - assertEquals(null, data) + assertEquals(null, dataOrNull) } with(items[1]) { assertEquals(Status.LOADING, status) - assertEquals(1, data!!.size) + assertEquals(1, dataOrNull?.size) } with(items[2]) { assertEquals(Status.SUCCESS, status) - assertEquals(1, data!!.size) + assertEquals(1, dataOrNull?.size) } - assertEquals(3.5, items[1].data?.single { it.subject == "Język polski" }!!.average, .0) // from details and after set custom plus/minus + assertEquals(3.5, items[1].dataOrNull?.single { it.subject == "Język polski" }?.average ?: 0.0, .0) // from details and after set custom plus/minus } @Test @@ -201,27 +215,27 @@ class GradeAverageProviderTest { coEvery { semesterRepository.getSemesters(student) } returns semesters coEvery { gradeRepository.getGrades(student, semesters[2], false) } returns flow { - emit(Resource.loading()) + emit(Resource.Loading()) delay(1000) - emit(Resource.success(secondGradeWithModifier to secondSummariesWithModifier)) + emit(Resource.Success(secondGradeWithModifier to secondSummariesWithModifier)) } coEvery { gradeRepository.getGrades(student, semesters[1], false) } returns flow { - emit(Resource.loading()) - emit(Resource.success(secondGradeWithModifier to secondSummariesWithModifier)) + emit(Resource.Loading()) + emit(Resource.Success(secondGradeWithModifier to secondSummariesWithModifier)) } val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId, false).toList() } with(items[0]) { assertEquals(Status.LOADING, status) - assertEquals(null, data) + assertEquals(null, dataOrNull) } with(items[1]) { assertEquals(Status.SUCCESS, status) - assertEquals(1, data!!.size) + assertEquals(1, dataOrNull?.size) } - assertEquals(3.5, items[1].data?.single { it.subject == "Język polski" }!!.average, .0) // from details and after set custom plus/minus + assertEquals(3.5, items[1].dataOrNull?.single { it.subject == "Język polski" }?.average ?: 0.0, .0) // from details and after set custom plus/minus } @Test @@ -230,12 +244,26 @@ class GradeAverageProviderTest { every { preferencesRepository.isOptionalArithmeticAverage } returns false every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.BOTH_SEMESTERS - coEvery { gradeRepository.getGrades(student, semesters[1], false) } returns flowWithResource { secondGradeWithModifier to secondSummariesWithModifier } - coEvery { gradeRepository.getGrades(student, semesters[2], false) } returns flowWithResource { - listOf(getGrade(semesters[2].semesterId, "Język polski", .0, .0, .0)) to listOf(getSummary(semesters[2].semesterId, "Język polski", 2.5)) + coEvery { + gradeRepository.getGrades( + student, + semesters[1], + false + ) + } returns resourceFlow { secondGradeWithModifier to secondSummariesWithModifier } + coEvery { gradeRepository.getGrades(student, semesters[2], false) } returns resourceFlow { + listOf(getGrade(semesters[2].semesterId, "Język polski", .0, .0, .0)) to listOf( + getSummary(semesters[2].semesterId, "Język polski", 2.5) + ) } - val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId, false).getResult() } + val items = runBlocking { + gradeAverageProvider.getGradesDetailsWithAverage( + student, + semesters[2].semesterId, + false + ).getResult() + } assertEquals(3.5, items.single { it.subject == "Język polski" }.average, .0) } @@ -246,10 +274,28 @@ class GradeAverageProviderTest { every { preferencesRepository.isOptionalArithmeticAverage } returns false every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.BOTH_SEMESTERS - coEvery { gradeRepository.getGrades(student, semesters[1], false) } returns flowWithResource { secondGradeWithModifier to secondSummariesWithModifier } - coEvery { gradeRepository.getGrades(student, semesters[2], false) } returns flowWithResource { emptyList() to listOf(getSummary(24, "Język polski", .0))} + coEvery { + gradeRepository.getGrades( + student, + semesters[1], + false + ) + } returns resourceFlow { secondGradeWithModifier to secondSummariesWithModifier } + coEvery { + gradeRepository.getGrades( + student, + semesters[2], + false + ) + } returns resourceFlow { emptyList() to listOf(getSummary(24, "Język polski", .0)) } - val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId, false).getResult() } + val items = runBlocking { + gradeAverageProvider.getGradesDetailsWithAverage( + student, + semesters[2].semesterId, + false + ).getResult() + } assertEquals(3.49, items.single { it.subject == "Język polski" }.average, .0) } @@ -260,10 +306,28 @@ class GradeAverageProviderTest { every { preferencesRepository.isOptionalArithmeticAverage } returns false every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.BOTH_SEMESTERS - coEvery { gradeRepository.getGrades(student, semesters[1], true) } returns flowWithResource { emptyList() to emptyList() } - coEvery { gradeRepository.getGrades(student, semesters[2], true) } returns flowWithResource { emptyList() to emptyList() } + coEvery { + gradeRepository.getGrades( + student, + semesters[1], + true + ) + } returns resourceFlow { emptyList() to emptyList() } + coEvery { + gradeRepository.getGrades( + student, + semesters[2], + true + ) + } returns resourceFlow { emptyList() to emptyList() } - val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId, true).getResult() } + val items = runBlocking { + gradeAverageProvider.getGradesDetailsWithAverage( + student, + semesters[2].semesterId, + true + ).getResult() + } assertEquals(0, items.size) } @@ -274,7 +338,13 @@ class GradeAverageProviderTest { every { preferencesRepository.isOptionalArithmeticAverage } returns false every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.ONE_SEMESTER coEvery { semesterRepository.getSemesters(student) } returns semesters - coEvery { gradeRepository.getGrades(student, semesters[2], true) } returns flowWithResource { secondGradeWithModifier to secondSummariesWithModifier } + coEvery { + gradeRepository.getGrades( + student, + semesters[2], + true + ) + } returns resourceFlow { secondGradeWithModifier to secondSummariesWithModifier } val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId, true).getResult() } @@ -292,7 +362,13 @@ class GradeAverageProviderTest { every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.ONE_SEMESTER coEvery { semesterRepository.getSemesters(student) } returns semesters - coEvery { gradeRepository.getGrades(student, semesters[2], true) } returns flowWithResource { secondGradeWithModifier to secondSummariesWithModifier } + coEvery { + gradeRepository.getGrades( + student, + semesters[2], + true + ) + } returns resourceFlow { secondGradeWithModifier to secondSummariesWithModifier } val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId, true).getResult() } @@ -310,7 +386,13 @@ class GradeAverageProviderTest { every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.ONE_SEMESTER coEvery { semesterRepository.getSemesters(student) } returns semesters - coEvery { gradeRepository.getGrades(student, semesters[2], true) } returns flowWithResource { secondGradeWithModifier to secondSummariesWithModifier } + coEvery { + gradeRepository.getGrades( + student, + semesters[2], + true + ) + } returns resourceFlow { secondGradeWithModifier to secondSummariesWithModifier } val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId, true).getResult() } @@ -328,7 +410,13 @@ class GradeAverageProviderTest { every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.ONE_SEMESTER coEvery { semesterRepository.getSemesters(student) } returns semesters - coEvery { gradeRepository.getGrades(student, semesters[2], true) } returns flowWithResource { secondGradeWithModifier to secondSummariesWithModifier } + coEvery { + gradeRepository.getGrades( + student, + semesters[2], + true + ) + } returns resourceFlow { secondGradeWithModifier to secondSummariesWithModifier } val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId, true).getResult() } @@ -340,7 +428,13 @@ class GradeAverageProviderTest { every { preferencesRepository.gradeAverageForceCalc } returns false every { preferencesRepository.isOptionalArithmeticAverage } returns false every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.ONE_SEMESTER - coEvery { gradeRepository.getGrades(student, semesters[2], true) } returns flowWithResource { secondGrades to secondSummaries } + coEvery { + gradeRepository.getGrades( + student, + semesters[2], + true + ) + } returns resourceFlow { secondGrades to secondSummaries } val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId, true).getResult() } @@ -354,7 +448,13 @@ class GradeAverageProviderTest { every { preferencesRepository.gradeAverageForceCalc } returns true every { preferencesRepository.isOptionalArithmeticAverage } returns false every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.ONE_SEMESTER - coEvery { gradeRepository.getGrades(student, semesters[2], true) } returns flowWithResource { secondGrades to secondSummaries } + coEvery { + gradeRepository.getGrades( + student, + semesters[2], + true + ) + } returns resourceFlow { secondGrades to secondSummaries } val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId, true).getResult() } @@ -368,7 +468,13 @@ class GradeAverageProviderTest { every { preferencesRepository.gradeAverageForceCalc } returns true every { preferencesRepository.isOptionalArithmeticAverage } returns false every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.ALL_YEAR - coEvery { gradeRepository.getGrades(student, semesters[1], true) } returns flowWithResource { firstGrades to firstSummaries } + coEvery { + gradeRepository.getGrades( + student, + semesters[1], + true + ) + } returns resourceFlow { firstGrades to firstSummaries } val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[1].semesterId, true).getResult() } @@ -384,29 +490,29 @@ class GradeAverageProviderTest { every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.ALL_YEAR coEvery { semesterRepository.getSemesters(student) } returns semesters coEvery { gradeRepository.getGrades(student, semesters[1], true) } returns flow { - emit(Resource.loading()) - emit(Resource.loading(firstGrades to firstSummaries)) - emit(Resource.success(firstGrades to firstSummaries)) + emit(Resource.Loading()) + emit(Resource.Intermediate(firstGrades to firstSummaries)) + emit(Resource.Success(firstGrades to firstSummaries)) } val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[1].semesterId, true).toList() } with(items[0]) { assertEquals(Status.LOADING, status) - assertEquals(null, data) + assertEquals(null, dataOrNull) } with(items[1]) { assertEquals(Status.LOADING, status) - assertEquals(2, data!!.size) + assertEquals(2, dataOrNull?.size) } with(items[2]) { assertEquals(Status.SUCCESS, status) - assertEquals(2, data!!.size) + assertEquals(2, dataOrNull?.size) } - assertEquals(2, items[2].data!!.size) - assertEquals(3.5, items[2].data!!.single { it.subject == "Matematyka" }.average, .0) // (from summary): 3,5 - assertEquals(3.5, items[2].data!!.single { it.subject == "Fizyka" }.average, .0) // (from summary): 3,5 + assertEquals(2, items[2].dataOrNull?.size) + assertEquals(3.5, items[2].dataOrNull?.single { it.subject == "Matematyka" }?.average ?: 0.0, .0) // (from summary): 3,5 + assertEquals(3.5, items[2].dataOrNull?.single { it.subject == "Fizyka" }?.average ?: 0.0, .0) // (from summary): 3,5 } @Test @@ -414,13 +520,13 @@ class GradeAverageProviderTest { every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.BOTH_SEMESTERS every { preferencesRepository.gradeAverageForceCalc } returns false every { preferencesRepository.isOptionalArithmeticAverage } returns false - coEvery { gradeRepository.getGrades(student, semesters[1], true) } returns flowWithResource { + coEvery { gradeRepository.getGrades(student, semesters[1], true) } returns resourceFlow { firstGrades to listOf( getSummary(22, "Matematyka", 3.0), getSummary(22, "Fizyka", 3.5) ) } - coEvery { gradeRepository.getGrades(student, semesters[2], true) } returns flowWithResource { + coEvery { gradeRepository.getGrades(student, semesters[2], true) } returns resourceFlow { secondGrades to listOf( getSummary(22, "Matematyka", 3.5), getSummary(22, "Fizyka", 4.0) @@ -440,46 +546,62 @@ class GradeAverageProviderTest { every { preferencesRepository.isOptionalArithmeticAverage } returns false every { preferencesRepository.gradeAverageForceCalc } returns false coEvery { gradeRepository.getGrades(student, semesters[1], true) } returns flow { - emit(Resource.loading()) - emit(Resource.loading(firstGrades to listOf( - getSummary(22, "Matematyka", 3.0), - getSummary(22, "Fizyka", 3.5) - ))) - emit(Resource.success(firstGrades to listOf( - getSummary(22, "Matematyka", 3.0), - getSummary(22, "Fizyka", 3.5) - ))) + emit(Resource.Loading()) + emit( + Resource.Intermediate( + firstGrades to listOf( + getSummary(22, "Matematyka", 3.0), + getSummary(22, "Fizyka", 3.5) + ) + ) + ) + emit( + Resource.Success( + firstGrades to listOf( + getSummary(22, "Matematyka", 3.0), + getSummary(22, "Fizyka", 3.5) + ) + ) + ) } coEvery { gradeRepository.getGrades(student, semesters[2], true) } returns flow { - emit(Resource.loading()) - emit(Resource.loading(secondGrades to listOf( - getSummary(22, "Matematyka", 3.5), - getSummary(22, "Fizyka", 4.0) - ))) - emit(Resource.success(secondGrades to listOf( - getSummary(22, "Matematyka", 3.5), - getSummary(22, "Fizyka", 4.0) - ))) + emit(Resource.Loading()) + emit( + Resource.Intermediate( + secondGrades to listOf( + getSummary(22, "Matematyka", 3.5), + getSummary(22, "Fizyka", 4.0) + ) + ) + ) + emit( + Resource.Success( + secondGrades to listOf( + getSummary(22, "Matematyka", 3.5), + getSummary(22, "Fizyka", 4.0) + ) + ) + ) } val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId, true).toList() } with(items[0]) { assertEquals(Status.LOADING, status) - assertEquals(null, data) + assertEquals(null, dataOrNull) } with(items[1]) { assertEquals(Status.LOADING, status) - assertEquals(2, data!!.size) + assertEquals(2, dataOrNull?.size) } with(items[2]) { assertEquals(Status.SUCCESS, status) - assertEquals(2, data!!.size) + assertEquals(2, dataOrNull?.size) } - assertEquals(2, items[2].data!!.size) - assertEquals(3.25, items[2].data!!.single { it.subject == "Matematyka" }.average, .0) // (from summaries ↑): 3,0 + 3,5 → 3,25 - assertEquals(3.75, items[2].data!!.single { it.subject == "Fizyka" }.average, .0) // (from summaries ↑): 3,5 + 4,0 → 3,75 + assertEquals(2, items[2].dataOrNull?.size) + assertEquals(3.25, items[2].dataOrNull?.single { it.subject == "Matematyka" }?.average ?: 0.0, .0) // (from summaries ↑): 3,0 + 3,5 → 3,25 + assertEquals(3.75, items[2].dataOrNull?.single { it.subject == "Fizyka" }?.average ?: 0.0, .0) // (from summaries ↑): 3,5 + 4,0 → 3,75 } @Test @@ -487,8 +609,14 @@ class GradeAverageProviderTest { every { preferencesRepository.gradeAverageForceCalc } returns true every { preferencesRepository.isOptionalArithmeticAverage } returns false every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.ALL_YEAR - coEvery { gradeRepository.getGrades(student, semesters[1], true) } returns flowWithResource { firstGrades to firstSummaries } - coEvery { gradeRepository.getGrades(student, semesters[2], true) } returns flowWithResource { + coEvery { + gradeRepository.getGrades( + student, + semesters[1], + true + ) + } returns resourceFlow { firstGrades to firstSummaries } + coEvery { gradeRepository.getGrades(student, semesters[2], true) } returns resourceFlow { secondGrades to listOf( getSummary(22, "Matematyka", 1.1), getSummary(22, "Fizyka", 7.26) @@ -527,7 +655,7 @@ class GradeAverageProviderTest { semesters[1], true ) - } returns flowWithResource { + } returns resourceFlow { firstGrades to listOf( getSummary(22, "Matematyka", .0), getSummary(22, "Fizyka", .0) @@ -539,7 +667,7 @@ class GradeAverageProviderTest { semesters[2], true ) - } returns flowWithResource { + } returns resourceFlow { secondGrades to listOf( getSummary(22, "Matematyka", .0), getSummary(22, "Fizyka", .0) @@ -566,40 +694,48 @@ class GradeAverageProviderTest { every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.ALL_YEAR coEvery { semesterRepository.getSemesters(student) } returns semesters coEvery { gradeRepository.getGrades(student, semesters[1], true) } returns flow { - emit(Resource.loading()) - emit(Resource.loading(firstGrades to firstSummaries)) - emit(Resource.success(firstGrades to firstSummaries)) + emit(Resource.Loading()) + emit(Resource.Intermediate(firstGrades to firstSummaries)) + emit(Resource.Success(firstGrades to firstSummaries)) } coEvery { gradeRepository.getGrades(student, semesters[2], true) } returns flow { - emit(Resource.loading()) - emit(Resource.loading(secondGrades to listOf( - getSummary(22, "Matematyka", 1.1), - getSummary(22, "Fizyka", 7.26) - ))) - emit(Resource.success(secondGrades to listOf( - getSummary(22, "Matematyka", 1.1), - getSummary(22, "Fizyka", 7.26) - ))) + emit(Resource.Loading()) + emit( + Resource.Intermediate( + secondGrades to listOf( + getSummary(22, "Matematyka", 1.1), + getSummary(22, "Fizyka", 7.26) + ) + ) + ) + emit( + Resource.Success( + secondGrades to listOf( + getSummary(22, "Matematyka", 1.1), + getSummary(22, "Fizyka", 7.26) + ) + ) + ) } val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId, true).toList() } with(items[0]) { assertEquals(Status.LOADING, status) - assertEquals(null, data) + assertEquals(null, dataOrNull) } with(items[1]) { assertEquals(Status.LOADING, status) - assertEquals(2, data!!.size) + assertEquals(2, dataOrNull?.size) } with(items[2]) { assertEquals(Status.SUCCESS, status) - assertEquals(2, data!!.size) + assertEquals(2, dataOrNull?.size) } - assertEquals(2, items[2].data!!.size) - assertEquals(3.0, items[2].data!!.single { it.subject == "Matematyka" }.average, .0) // (from details): 3,5 + 2,5 → 3,0 - assertEquals(3.25, items[2].data!!.single { it.subject == "Fizyka" }.average, .0) // (from details): 3,5 + 3,0 → 3,25 + assertEquals(2, items[2].dataOrNull?.size) + assertEquals(3.0, items[2].dataOrNull?.single { it.subject == "Matematyka" }?.average ?: 0.0, .0) // (from details): 3,5 + 2,5 → 3,0 + assertEquals(3.25, items[2].dataOrNull?.single { it.subject == "Fizyka" }?.average ?: 0.0, .0) // (from details): 3,5 + 3,0 → 3,25 } @Test @@ -614,14 +750,14 @@ class GradeAverageProviderTest { semesters[1], true ) - } returns flowWithResource { firstGrades to emptyList() } + } returns resourceFlow { firstGrades to emptyList() } coEvery { gradeRepository.getGrades( student, semesters[2], true ) - } returns flowWithResource { secondGrades to emptyList() } + } returns resourceFlow { secondGrades to emptyList() } val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage( @@ -646,14 +782,40 @@ class GradeAverageProviderTest { every { preferencesRepository.isOptionalArithmeticAverage } returns false every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.ALL_YEAR - coEvery { gradeRepository.getGrades(student, semesters[1], true) } returns flowWithResource { firstGrades to emptyList() } - coEvery { gradeRepository.getGrades(student, semesters[2], true) } returns flowWithResource { secondGrades to emptyList() } + coEvery { + gradeRepository.getGrades( + student, + semesters[1], + true + ) + } returns resourceFlow { firstGrades to emptyList() } + coEvery { + gradeRepository.getGrades( + student, + semesters[2], + true + ) + } returns resourceFlow { secondGrades to emptyList() } - val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId, true).getResult() } + val items = runBlocking { + gradeAverageProvider.getGradesDetailsWithAverage( + student, + semesters[2].semesterId, + true + ).getResult() + } assertEquals(2, items.size) - assertEquals(3.0, items.single { it.subject == "Matematyka" }.average, .0) // (from details): 3,5 + 2,5 → 3,0 - assertEquals(3.25, items.single { it.subject == "Fizyka" }.average, .0) // (from details): 3,5 + 3,0 → 3,25 + assertEquals( + 3.0, + items.single { it.subject == "Matematyka" }.average, + .0 + ) // (from details): 3,5 + 2,5 → 3,0 + assertEquals( + 3.25, + items.single { it.subject == "Fizyka" }.average, + .0 + ) // (from details): 3,5 + 3,0 → 3,25 } @Test @@ -662,12 +824,12 @@ class GradeAverageProviderTest { every { preferencesRepository.isOptionalArithmeticAverage } returns false every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.BOTH_SEMESTERS - coEvery { gradeRepository.getGrades(student, semesters[1], true) } returns flowWithResource { + coEvery { gradeRepository.getGrades(student, semesters[1], true) } returns resourceFlow { firstGrades to listOf( getSummary(22, "Matematyka", 4.0) ) } - coEvery { gradeRepository.getGrades(student, semesters[2], true) } returns flowWithResource { + coEvery { gradeRepository.getGrades(student, semesters[2], true) } returns resourceFlow { secondGrades to listOf( getSummary(23, "Matematyka", 3.0) ) @@ -686,14 +848,40 @@ class GradeAverageProviderTest { every { preferencesRepository.isOptionalArithmeticAverage } returns false every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.BOTH_SEMESTERS - coEvery { gradeRepository.getGrades(student, semesters[1], true) } returns flowWithResource { firstGrades to firstSummaries } - coEvery { gradeRepository.getGrades(student, semesters[2], true) } returns flowWithResource { secondGrades to secondSummaries.dropLast(1) } + coEvery { + gradeRepository.getGrades( + student, + semesters[1], + true + ) + } returns resourceFlow { firstGrades to firstSummaries } + coEvery { + gradeRepository.getGrades( + student, + semesters[2], + true + ) + } returns resourceFlow { secondGrades to secondSummaries.dropLast(1) } - val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId, true).getResult() } + val items = runBlocking { + gradeAverageProvider.getGradesDetailsWithAverage( + student, + semesters[2].semesterId, + true + ).getResult() + } assertEquals(2, items.size) - assertEquals(3.4, items.single { it.subject == "Matematyka" }.average, .0) // (from summaries): 3,9 + 2,9 → 3,4 - assertEquals(3.05, items.single { it.subject == "Fizyka" }.average, .0) // 3,1 (from summary) + 3,0 (from details) → 3,05 + assertEquals( + 3.4, + items.single { it.subject == "Matematyka" }.average, + .0 + ) // (from summaries): 3,9 + 2,9 → 3,4 + assertEquals( + 3.05, + items.single { it.subject == "Fizyka" }.average, + .0 + ) // 3,1 (from summary) + 3,0 (from details) → 3,05 } @Test @@ -702,14 +890,40 @@ class GradeAverageProviderTest { every { preferencesRepository.isOptionalArithmeticAverage } returns false every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.BOTH_SEMESTERS - coEvery { gradeRepository.getGrades(student, semesters[1], true) } returns flowWithResource { firstGrades to firstSummaries.dropLast(1) } - coEvery { gradeRepository.getGrades(student, semesters[2], true) } returns flowWithResource { secondGrades to secondSummaries } + coEvery { + gradeRepository.getGrades( + student, + semesters[1], + true + ) + } returns resourceFlow { firstGrades to firstSummaries.dropLast(1) } + coEvery { + gradeRepository.getGrades( + student, + semesters[2], + true + ) + } returns resourceFlow { secondGrades to secondSummaries } - val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId, true).getResult() } + val items = runBlocking { + gradeAverageProvider.getGradesDetailsWithAverage( + student, + semesters[2].semesterId, + true + ).getResult() + } assertEquals(2, items.size) - assertEquals(3.4, items.single { it.subject == "Matematyka" }.average, .0) // (from summaries): 3,9 + 2,9 → 3,4 - assertEquals(3.45, items.single { it.subject == "Fizyka" }.average, .0) // 3,5 (from details) + 3,4 (from summary) → 3,45 + assertEquals( + 3.4, + items.single { it.subject == "Matematyka" }.average, + .0 + ) // (from summaries): 3,9 + 2,9 → 3,4 + assertEquals( + 3.45, + items.single { it.subject == "Fizyka" }.average, + .0 + ) // 3,5 (from details) + 3,4 (from summary) → 3,45 } @Test @@ -718,14 +932,40 @@ class GradeAverageProviderTest { every { preferencesRepository.isOptionalArithmeticAverage } returns false every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.ALL_YEAR - coEvery { gradeRepository.getGrades(student, semesters[1], true) } returns flowWithResource { firstGrades to firstSummaries.dropLast(1) } - coEvery { gradeRepository.getGrades(student, semesters[2], true) } returns flowWithResource { secondGrades to secondSummaries } + coEvery { + gradeRepository.getGrades( + student, + semesters[1], + true + ) + } returns resourceFlow { firstGrades to firstSummaries.dropLast(1) } + coEvery { + gradeRepository.getGrades( + student, + semesters[2], + true + ) + } returns resourceFlow { secondGrades to secondSummaries } - val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId, true).getResult() } + val items = runBlocking { + gradeAverageProvider.getGradesDetailsWithAverage( + student, + semesters[2].semesterId, + true + ).getResult() + } assertEquals(2, items.size) - assertEquals(3.0, items.single { it.subject == "Matematyka" }.average, .0) // (from details): 3,5 + 2,5 → 3,0 - assertEquals(3.25, items.single { it.subject == "Fizyka" }.average, .0) // (from details): 3,5 + 3,0 → 3,25 + assertEquals( + 3.0, + items.single { it.subject == "Matematyka" }.average, + .0 + ) // (from details): 3,5 + 2,5 → 3,0 + assertEquals( + 3.25, + items.single { it.subject == "Fizyka" }.average, + .0 + ) // (from details): 3,5 + 3,0 → 3,25 } @Test @@ -734,7 +974,7 @@ class GradeAverageProviderTest { every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.BOTH_SEMESTERS every { preferencesRepository.isOptionalArithmeticAverage } returns false - coEvery { gradeRepository.getGrades(student, semesters[1], true) } returns flowWithResource { + coEvery { gradeRepository.getGrades(student, semesters[1], true) } returns resourceFlow { listOf( getGrade(22, "Fizyka", 5.0, weight = 2.0), getGrade(22, "Fizyka", 6.0, weight = 2.0), @@ -746,7 +986,7 @@ class GradeAverageProviderTest { getGrade(22, "Fizyka", 6.0, weight = 2.0) ) to listOf(getSummary(semesterId = 22, subject = "Fizyka", average = .0)) } - coEvery { gradeRepository.getGrades(student, semesters[2], true) } returns flowWithResource { + coEvery { gradeRepository.getGrades(student, semesters[2], true) } returns resourceFlow { listOf( getGrade(23, "Fizyka", 5.0, weight = 1.0), getGrade(23, "Fizyka", 5.0, weight = 2.0), @@ -765,7 +1005,7 @@ class GradeAverageProviderTest { every { preferencesRepository.isOptionalArithmeticAverage } returns false every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.ALL_YEAR - coEvery { gradeRepository.getGrades(student, semesters[1], true) } returns flowWithResource { + coEvery { gradeRepository.getGrades(student, semesters[1], true) } returns resourceFlow { listOf( getGrade(22, "Fizyka", 5.0, weight = 2.0), getGrade(22, "Fizyka", 6.0, weight = 2.0), @@ -777,7 +1017,7 @@ class GradeAverageProviderTest { getGrade(22, "Fizyka", 6.0, weight = 2.0) ) to listOf(getSummary(semesterId = 22, subject = "Fizyka", average = .0)) } - coEvery { gradeRepository.getGrades(student, semesters[2], true) } returns flowWithResource { + coEvery { gradeRepository.getGrades(student, semesters[2], true) } returns resourceFlow { listOf( getGrade(23, "Fizyka", 5.0, weight = 1.0), getGrade(23, "Fizyka", 5.0, weight = 2.0), @@ -802,7 +1042,7 @@ class GradeAverageProviderTest { every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.BOTH_SEMESTERS coEvery { semesterRepository.getSemesters(student) } returns semesters - coEvery { gradeRepository.getGrades(student, semesters[1], true) } returns flowWithResource { + coEvery { gradeRepository.getGrades(student, semesters[1], true) } returns resourceFlow { listOf( getGrade(22, "Fizyka", 5.0, weight = 2.0), getGrade(22, "Fizyka", 6.0, weight = 2.0), @@ -814,7 +1054,7 @@ class GradeAverageProviderTest { getGrade(22, "Fizyka", 6.0, weight = 2.0) ) to listOf(getSummary(semesterId = 22, subject = "Fizyka", average = .0)) } - coEvery { gradeRepository.getGrades(student, semesters[2], true) } returns flowWithResource { + coEvery { gradeRepository.getGrades(student, semesters[2], true) } returns resourceFlow { listOf( getGrade(23, "Fizyka", 5.0, weight = 1.0), getGrade(23, "Fizyka", 5.0, weight = 2.0), @@ -839,7 +1079,7 @@ class GradeAverageProviderTest { every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.ALL_YEAR coEvery { semesterRepository.getSemesters(student) } returns semesters - coEvery { gradeRepository.getGrades(student, semesters[1], true) } returns flowWithResource { + coEvery { gradeRepository.getGrades(student, semesters[1], true) } returns resourceFlow { listOf( getGrade(22, "Fizyka", 5.0, weight = 2.0), getGrade(22, "Fizyka", 6.0, weight = 2.0), @@ -851,7 +1091,7 @@ class GradeAverageProviderTest { getGrade(22, "Fizyka", 6.0, weight = 2.0) ) to listOf(getSummary(semesterId = 22, subject = "Fizyka", average = .0)) } - coEvery { gradeRepository.getGrades(student, semesters[2], true) } returns flowWithResource { + coEvery { gradeRepository.getGrades(student, semesters[2], true) } returns resourceFlow { listOf( getGrade(23, "Fizyka", 5.0, weight = 1.0), getGrade(23, "Fizyka", 5.0, weight = 2.0), @@ -859,12 +1099,51 @@ class GradeAverageProviderTest { ) to listOf(getSummary(semesterId = 23, subject = "Fizyka", average = .0)) } - val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId, true).getResult() } + val items = runBlocking { + gradeAverageProvider.getGradesDetailsWithAverage( + student, + semesters[2].semesterId, + true + ).getResult() + } - assertEquals(5.5555, items.single { it.subject == "Fizyka" }.average, .0001) // (from details): 5.72727272 + 4,8 → .average() + assertEquals( + 5.5555, + items.single { it.subject == "Fizyka" }.average, + .0001 + ) // (from details): 5.72727272 + 4,8 → .average() } - private fun getGrade(semesterId: Int, subject: String, value: Double, modifier: Double = 0.0, weight: Double = 1.0, entry: String = ""): Grade { + @Test + fun `calc both semesters average when both summary have same average from vulcan and second semester has no grades`() { + every { preferencesRepository.gradeAverageForceCalc } returns false + every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.BOTH_SEMESTERS + every { preferencesRepository.isOptionalArithmeticAverage } returns false + + coEvery { gradeRepository.getGrades(student, semesters[1], true) } returns + resourceFlow { firstGrades to firstSummaries } + coEvery { gradeRepository.getGrades(student, semesters[2], true) } returns + resourceFlow { listOf() to firstSummaries } + + val items = runBlocking { + gradeAverageProvider.getGradesDetailsWithAverage( + student, + semesters[2].semesterId, + true + ).getResult() + } + + assertEquals(3.1, items.single { it.subject == "Fizyka" }.average, .0001) + } + + private fun getGrade( + semesterId: Int, + subject: String, + value: Double, + modifier: Double = 0.0, + weight: Double = 1.0, + entry: String = "" + ): Grade { return Grade( studentId = 101, semesterId = semesterId, diff --git a/app/src/test/java/io/github/wulkanowy/ui/modules/login/LoginPresenterTest.kt b/app/src/test/java/io/github/wulkanowy/ui/modules/login/LoginPresenterTest.kt deleted file mode 100644 index 35cda35bd..000000000 --- a/app/src/test/java/io/github/wulkanowy/ui/modules/login/LoginPresenterTest.kt +++ /dev/null @@ -1,57 +0,0 @@ -package io.github.wulkanowy.ui.modules.login - -import io.github.wulkanowy.data.repositories.StudentRepository -import io.mockk.MockKAnnotations -import io.mockk.clearMocks -import io.mockk.every -import io.mockk.impl.annotations.MockK -import io.mockk.verify -import org.junit.Assert.assertNotEquals -import org.junit.Before -import org.junit.Test - -class LoginPresenterTest { - - @MockK(relaxed = true) - lateinit var loginView: LoginView - - @MockK(relaxed = true) - lateinit var errorHandler: LoginErrorHandler - - @MockK - lateinit var studentRepository: StudentRepository - - private lateinit var presenter: LoginPresenter - - @Before - fun initPresenter() { - MockKAnnotations.init(this) - clearMocks(loginView) - - presenter = LoginPresenter(errorHandler, studentRepository) - presenter.onAttachView(loginView) - } - - @Test - fun initViewTest() { - verify { loginView.initView() } - verify { loginView.showActionBar(false) } - } - - @Test - fun onBackPressedTest() { - clearMocks(loginView) - every { loginView.currentViewIndex } returns 1 - presenter.onBackPressed { } - verify { loginView.switchView(0) } - } - - @Test - fun onBackPressedDefaultTest() { - var i = 0 - every { loginView.currentViewIndex } returns 0 - presenter.onBackPressed { i++ } - assertNotEquals(0, i) - } -} - diff --git a/app/src/test/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenterTest.kt b/app/src/test/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenterTest.kt index 67d3e6266..9bcfb8b6c 100644 --- a/app/src/test/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenterTest.kt +++ b/app/src/test/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenterTest.kt @@ -6,18 +6,13 @@ import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.ui.modules.login.LoginErrorHandler import io.github.wulkanowy.utils.AnalyticsHelper -import io.mockk.MockKAnnotations -import io.mockk.Runs -import io.mockk.coEvery -import io.mockk.every +import io.mockk.* import io.mockk.impl.annotations.MockK -import io.mockk.just -import io.mockk.verify import org.junit.Before import org.junit.Rule import org.junit.Test import java.io.IOException -import java.time.LocalDateTime.now +import java.time.Instant class LoginFormPresenterTest { @@ -121,7 +116,7 @@ class LoginFormPresenterTest { classId = 1, isCurrent = false, symbol = "", - registrationDate = now(), + registrationDate = Instant.now(), className = "", mobileBaseUrl = "", privateKey = "", diff --git a/app/src/test/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenterTest.kt b/app/src/test/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenterTest.kt index c79d739e8..e52ec3ae2 100644 --- a/app/src/test/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenterTest.kt +++ b/app/src/test/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenterTest.kt @@ -4,20 +4,15 @@ import io.github.wulkanowy.MainCoroutineRule import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.data.repositories.StudentRepository +import io.github.wulkanowy.services.sync.SyncManager import io.github.wulkanowy.ui.modules.login.LoginErrorHandler import io.github.wulkanowy.utils.AnalyticsHelper -import io.mockk.MockKAnnotations -import io.mockk.Runs -import io.mockk.clearMocks -import io.mockk.coEvery -import io.mockk.every +import io.mockk.* import io.mockk.impl.annotations.MockK -import io.mockk.just -import io.mockk.verify import org.junit.Before import org.junit.Rule import org.junit.Test -import java.time.LocalDateTime.now +import java.time.Instant class LoginStudentSelectPresenterTest { @@ -36,6 +31,9 @@ class LoginStudentSelectPresenterTest { @MockK(relaxed = true) lateinit var analytics: AnalyticsHelper + @MockK(relaxed = true) + lateinit var syncManager: SyncManager + private lateinit var presenter: LoginStudentSelectPresenter private val testStudent by lazy { @@ -51,7 +49,7 @@ class LoginStudentSelectPresenterTest { schoolSymbol = "", classId = 1, studentName = "", - registrationDate = now(), + registrationDate = Instant.now(), className = "", loginMode = "", certificateKey = "", @@ -77,8 +75,8 @@ class LoginStudentSelectPresenterTest { every { loginStudentSelectView.showProgress(any()) } just Runs every { loginStudentSelectView.showContent(any()) } just Runs - presenter = LoginStudentSelectPresenter(studentRepository, errorHandler, analytics) - presenter.onAttachView(loginStudentSelectView, null) + presenter = LoginStudentSelectPresenter(studentRepository, errorHandler, syncManager, analytics) + presenter.onAttachView(loginStudentSelectView, emptyList()) } @Test diff --git a/app/src/test/java/io/github/wulkanowy/ui/modules/main/MainPresenterTest.kt b/app/src/test/java/io/github/wulkanowy/ui/modules/main/MainPresenterTest.kt index 7557d745a..6cfab1995 100644 --- a/app/src/test/java/io/github/wulkanowy/ui/modules/main/MainPresenterTest.kt +++ b/app/src/test/java/io/github/wulkanowy/ui/modules/main/MainPresenterTest.kt @@ -4,14 +4,12 @@ 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.ErrorHandler +import io.github.wulkanowy.utils.AdsHelper import io.github.wulkanowy.utils.AnalyticsHelper -import io.mockk.MockKAnnotations -import io.mockk.Runs -import io.mockk.clearMocks -import io.mockk.every +import io.github.wulkanowy.utils.AppInfo +import io.mockk.* import io.mockk.impl.annotations.MockK -import io.mockk.just -import io.mockk.verify +import kotlinx.serialization.json.Json import org.junit.Before import org.junit.Test @@ -35,6 +33,12 @@ class MainPresenterTest { @MockK(relaxed = true) lateinit var analytics: AnalyticsHelper + @MockK(relaxed = true) + lateinit var appInfo: AppInfo + + @MockK(relaxed = true) + lateinit var adsHelper: AdsHelper + private lateinit var presenter: MainPresenter @Before @@ -43,8 +47,16 @@ class MainPresenterTest { clearMocks(mainView) every { mainView.initView(any(), any()) } just Runs - presenter = - MainPresenter(errorHandler, studentRepository, prefRepository, syncManager, analytics) + presenter = MainPresenter( + errorHandler = errorHandler, + studentRepository = studentRepository, + preferencesRepository = prefRepository, + syncManager = syncManager, + analytics = analytics, + json = Json, + appInfo = appInfo, + adsHelper = adsHelper + ) presenter.onAttachView(mainView, null) } diff --git a/app/src/test/java/io/github/wulkanowy/ui/modules/splash/SplashPresenterTest.kt b/app/src/test/java/io/github/wulkanowy/ui/modules/splash/SplashPresenterTest.kt index 323119742..eb362978d 100644 --- a/app/src/test/java/io/github/wulkanowy/ui/modules/splash/SplashPresenterTest.kt +++ b/app/src/test/java/io/github/wulkanowy/ui/modules/splash/SplashPresenterTest.kt @@ -7,6 +7,7 @@ import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.impl.annotations.MockK import io.mockk.verify +import kotlinx.serialization.json.Json import org.junit.Before import org.junit.Rule import org.junit.Test @@ -30,7 +31,7 @@ class SplashPresenterTest { @Before fun setUp() { MockKAnnotations.init(this) - presenter = SplashPresenter(errorHandler, studentRepository) + presenter = SplashPresenter(errorHandler, studentRepository, Json) } @Test diff --git a/app/src/test/java/io/github/wulkanowy/utils/ResourceUtils.kt b/app/src/test/java/io/github/wulkanowy/utils/ResourceUtils.kt new file mode 100644 index 000000000..60df1db09 --- /dev/null +++ b/app/src/test/java/io/github/wulkanowy/utils/ResourceUtils.kt @@ -0,0 +1,14 @@ +package io.github.wulkanowy.utils + +import io.github.wulkanowy.data.Resource + +enum class Status { + LOADING, SUCCESS, ERROR +} + +val Resource.status + get() = when (this) { + is Resource.Error -> Status.ERROR + is Resource.Loading -> Status.LOADING + is Resource.Success -> Status.SUCCESS + } \ No newline at end of file diff --git a/app/src/test/java/io/github/wulkanowy/utils/SemesterExtensionKtTest.kt b/app/src/test/java/io/github/wulkanowy/utils/SemesterExtensionKtTest.kt new file mode 100644 index 000000000..b7d3ecc94 --- /dev/null +++ b/app/src/test/java/io/github/wulkanowy/utils/SemesterExtensionKtTest.kt @@ -0,0 +1,72 @@ +package io.github.wulkanowy.utils + +import io.github.wulkanowy.data.db.entities.Semester +import io.github.wulkanowy.getSemesterEntity +import org.junit.Test +import java.time.LocalDate +import kotlin.test.assertEquals + +class SemesterExtensionKtTest { + + @Test(expected = IllegalArgumentException::class) + fun `get current semester when current is doubled`() { + val semesters = listOf( + getSemesterEntity(1, 1, LocalDate.now(), LocalDate.now()), + getSemesterEntity(1, 1, LocalDate.now(), LocalDate.now()) + ) + + semesters.getCurrentOrLast() + } + + @Test(expected = RuntimeException::class) + fun `get current semester when there is empty list`() { + val semesters = listOf() + + semesters.getCurrentOrLast() + } + + @Test + fun `get current kindergarten semester when there is no any current`() { + val semesters = listOf( + createSemesterEntity( + kindergartenDiaryId = 281, + schoolYear = 2020, + semesterId = 0, + start = LocalDate.of(2020, 9, 1), + end = LocalDate.of(2021, 8, 31), + ), + createSemesterEntity( + kindergartenDiaryId = 342, + schoolYear = 2021, + semesterId = 0, + start = LocalDate.of(2021, 9, 1), + end = LocalDate.of(2022, 8, 31), + ), + ) + + val res = semesters.getCurrentOrLast() + + assertEquals(2021, res.schoolYear) + } + + private fun createSemesterEntity( + diaryId: Int = 0, + kindergartenDiaryId: Int = 0, + semesterId: Int = 0, + schoolYear: Int = 0, + start: LocalDate = LocalDate.now(), + end: LocalDate = LocalDate.now().plusMonths(6), + ) = Semester( + studentId = 1, + diaryId = diaryId, + kindergartenDiaryId = kindergartenDiaryId, + semesterId = semesterId, + diaryName = "$semesterId", + schoolYear = schoolYear, + classId = 0, + semesterName = semesterId, + unitId = 1, + start = start, + end = end + ) +} diff --git a/app/src/test/java/io/github/wulkanowy/utils/TimeExtensionTest.kt b/app/src/test/java/io/github/wulkanowy/utils/TimeExtensionTest.kt index 0ffbf78ec..70ad6d534 100644 --- a/app/src/test/java/io/github/wulkanowy/utils/TimeExtensionTest.kt +++ b/app/src/test/java/io/github/wulkanowy/utils/TimeExtensionTest.kt @@ -1,13 +1,13 @@ package io.github.wulkanowy.utils -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue +import org.junit.Assert.* import org.junit.Test +import java.time.Instant import java.time.LocalDate.of import java.time.LocalDateTime import java.time.Month.JANUARY -import java.util.Locale +import java.time.ZoneOffset +import java.util.* class TimeExtensionTest { @@ -23,9 +23,16 @@ class TimeExtensionTest { } @Test - fun toFormattedStringLocalDateTimeTest() { - assertEquals("01.10.2018", LocalDateTime.of(2018, 10, 1, 10, 0, 0).toFormattedString()) - assertEquals("2018-10-01 10:00:00", LocalDateTime.of(2018, 10, 1, 10, 0, 0).toFormattedString("uuuu-MM-dd HH:mm:ss")) + fun toFormattedStringFromInstantTest() { + assertEquals( + "01.10.2018", + LocalDateTime.of(2018, 10, 1, 10, 0, 0).toInstant(ZoneOffset.UTC).toFormattedString() + ) + assertEquals( + "2018-10-01 10:00:00", + LocalDateTime.of(2018, 10, 1, 10, 0, 0).toInstant(ZoneOffset.UTC) + .toFormattedString("uuuu-MM-dd HH:mm:ss", ZoneOffset.UTC) + ) } @Test @@ -222,4 +229,25 @@ class TimeExtensionTest { assertEquals(of(2020, 10, 18), endExamsDay) } } + + @Test + fun getLocalDateToTimestamp() { + assertEquals(0L, of(1970, 1, 1).toTimestamp()) + assertEquals(946684800000L, of(2000, 1, 1).toTimestamp()) + assertEquals(1640131200000L, of(2021, 12, 22).toTimestamp()) + } + + @Test + fun getLocalDateFromInstant() { + assertEquals(of(1970, 1, 1), Instant.ofEpochMilli(0).toLocalDate()) + assertEquals(of(2000, 1, 1), Instant.ofEpochMilli(946684800000).toLocalDate()) + assertEquals(of(2021, 12, 22), Instant.ofEpochMilli(1640131200000L).toLocalDate()) + } + + @Test + fun timestampToLocalDateTime() { + assertEquals(LocalDateTime.of(1970, 1, 1, 0, 0, 0), 0L.toLocalDateTime()) + assertEquals(LocalDateTime.of(2000, 1, 1, 0, 0, 0), 946684800000.toLocalDateTime()) + assertEquals(LocalDateTime.of(2021, 12, 22, 0, 0, 0), 1640131200000L.toLocalDateTime()) + } } diff --git a/app/src/test/java/io/github/wulkanowy/utils/TimetableExtensionTest.kt b/app/src/test/java/io/github/wulkanowy/utils/TimetableExtensionTest.kt index b7fa58c6f..a4d649e26 100644 --- a/app/src/test/java/io/github/wulkanowy/utils/TimetableExtensionTest.kt +++ b/app/src/test/java/io/github/wulkanowy/utils/TimetableExtensionTest.kt @@ -6,9 +6,8 @@ import org.junit.Assert.assertFalse import org.junit.Assert.assertNotEquals import org.junit.Assert.assertTrue import org.junit.Test -import java.time.LocalDate -import java.time.LocalDateTime -import java.time.LocalDateTime.now +import java.time.* +import java.time.Duration.ofMinutes class TimetableExtensionTest { @@ -17,52 +16,38 @@ class TimetableExtensionTest { assertFalse(getTimetableEntity().isShowTimeUntil(null)) assertFalse(getTimetableEntity(isStudentPlan = false).isShowTimeUntil(null)) assertFalse(getTimetableEntity(isStudentPlan = true, canceled = true).isShowTimeUntil(null)) - assertFalse(getTimetableEntity(isStudentPlan = true, canceled = false, start = now().minusSeconds(1)).isShowTimeUntil(null)) - assertFalse(getTimetableEntity(isStudentPlan = true, canceled = false, start = now().plusMinutes(5)).isShowTimeUntil(now().plusMinutes(5))) - assertFalse(getTimetableEntity(isStudentPlan = true, canceled = false, start = now().plusMinutes(61)).isShowTimeUntil(now().minusMinutes(5))) + assertFalse(getTimetableEntity(isStudentPlan = true, canceled = false, start = Instant.now().minusSeconds(1)).isShowTimeUntil(null)) + assertFalse(getTimetableEntity(isStudentPlan = true, canceled = false, start = Instant.now().plus(ofMinutes(5))).isShowTimeUntil(Instant.now().plus(ofMinutes(5)))) + assertFalse(getTimetableEntity(isStudentPlan = true, canceled = false, start = Instant.now().plus(ofMinutes(61))).isShowTimeUntil(Instant.now().minus(ofMinutes(5)))) - assertTrue(getTimetableEntity(isStudentPlan = true, canceled = false, start = now().plusMinutes(60)).isShowTimeUntil(now().minusMinutes(5))) - assertTrue(getTimetableEntity(isStudentPlan = true, canceled = false, start = now().plusMinutes(60)).isShowTimeUntil(null)) + assertTrue(getTimetableEntity(isStudentPlan = true, canceled = false, start = Instant.now().plus(ofMinutes(60))).isShowTimeUntil(Instant.now().minus(ofMinutes(5)))) + assertTrue(getTimetableEntity(isStudentPlan = true, canceled = false, start = Instant.now().plus(ofMinutes(60))).isShowTimeUntil(null)) - assertFalse(getTimetableEntity(isStudentPlan = true, canceled = false, start = now().minusSeconds(1)).isShowTimeUntil(null)) + assertFalse(getTimetableEntity(isStudentPlan = true, canceled = false, start = Instant.now().minusSeconds(1)).isShowTimeUntil(null)) } @Test fun getLeft() { assertEquals(null, getTimetableEntity(canceled = true).left) - assertEquals(null, getTimetableEntity(start = now().plusMinutes(5), end = now().plusMinutes(50)).left) - assertEquals(null, getTimetableEntity(start = now().minusMinutes(1), end = now().plusMinutes(44), isStudentPlan = false).left) - assertNotEquals( - null, - getTimetableEntity( - start = now().minusMinutes(1), - end = now().plusMinutes(44), - isStudentPlan = true - ).left - ) - assertNotEquals( - null, - getTimetableEntity( - start = now(), - end = now().plusMinutes(45), - isStudentPlan = true - ).left - ) + assertEquals(null, getTimetableEntity(start = Instant.now().plus(ofMinutes(5)), end = Instant.now().plus(ofMinutes(50))).left) + assertEquals(null, getTimetableEntity(start = Instant.now().minus(ofMinutes(1)), end = Instant.now().plus(ofMinutes(44)), isStudentPlan = false).left) + assertNotEquals(null, getTimetableEntity(start = Instant.now().minus(ofMinutes(1)), end = Instant.now().plus(ofMinutes(44)), isStudentPlan = true).left) + assertNotEquals(null, getTimetableEntity(start = Instant.now(), end = Instant.now().plus(ofMinutes(45)), isStudentPlan = true).left) } @Test fun isJustFinished() { - assertFalse(getTimetableEntity(end = now().minusSeconds(16)).isJustFinished) - assertTrue(getTimetableEntity(end = now().minusSeconds(14)).isJustFinished) - assertTrue(getTimetableEntity(end = now().minusSeconds(1)).isJustFinished) - assertFalse(getTimetableEntity(end = now().plusSeconds(1)).isJustFinished) + assertFalse(getTimetableEntity(end = Instant.now().minusSeconds(16)).isJustFinished) + assertTrue(getTimetableEntity(end = Instant.now().minusSeconds(14)).isJustFinished) + assertTrue(getTimetableEntity(end = Instant.now().minusSeconds(1)).isJustFinished) + assertFalse(getTimetableEntity(end = Instant.now().plusSeconds(1)).isJustFinished) } private fun getTimetableEntity( isStudentPlan: Boolean = false, canceled: Boolean = false, - start: LocalDateTime = now(), - end: LocalDateTime = now() + start: Instant = Instant.now(), + end: Instant = Instant.now() ) = Timetable( studentId = 0, subject = "", diff --git a/build.gradle b/build.gradle index bd74bab41..98c9dfb84 100644 --- a/build.gradle +++ b/build.gradle @@ -1,8 +1,8 @@ buildscript { ext { - kotlin_version = '1.6.0' - about_libraries = '8.9.4' - hilt_version = "2.40.5" + kotlin_version = '1.7.10' + about_libraries = '10.4.0' + hilt_version = "2.43.2" } repositories { mavenCentral() @@ -13,14 +13,14 @@ buildscript { dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" - classpath 'com.android.tools.build:gradle:7.0.4' + classpath 'com.android.tools.build:gradle:7.2.2' classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" - classpath 'com.google.gms:google-services:4.3.10' - classpath 'com.huawei.agconnect:agcp:1.6.2.300' - classpath 'com.google.firebase:firebase-crashlytics-gradle:2.8.1' + classpath 'com.google.gms:google-services:4.3.13' + classpath 'com.huawei.agconnect:agcp:1.7.1.300' + classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.1' classpath "com.github.triplet.gradle:play-publisher:3.6.0" - classpath "ru.cian:huawei-publish-gradle-plugin:1.3.0" - classpath "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.3" + classpath "ru.cian:huawei-publish-gradle-plugin:1.3.4" + classpath "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.4.0.2513" classpath "gradle.plugin.com.star-zero.gradle:githook:1.2.0" classpath "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:$about_libraries" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a0f7639f7..d7e66b5c6 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.1-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 744e882ed..1b6c78733 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ -#!/usr/bin/env sh +#!/bin/sh # -# Copyright 2015 the original author or authors. +# Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,67 +17,101 @@ # ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## # Attempt to set APP_HOME + # Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` +APP_BASE_NAME=${0##*/} # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MSYS* | MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar @@ -87,9 +121,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -98,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" + JAVACMD=java which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the @@ -106,80 +140,95 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=`expr $i + 1` - done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' exec "$JAVACMD" "$@"