diff --git a/.github/workflows/deploy-store.yml b/.github/workflows/deploy-store.yml index 3ce618ca7..e8237a381 100644 --- a/.github/workflows/deploy-store.yml +++ b/.github/workflows/deploy-store.yml @@ -13,10 +13,10 @@ jobs: environment: google-play steps: - uses: actions/checkout@v3 - - uses: actions/setup-java@v2 + - uses: actions/setup-java@v3 with: distribution: 'zulu' - java-version: 11 + java-version: 17 - uses: actions/cache@v3 with: path: | @@ -49,10 +49,10 @@ jobs: environment: app-gallery steps: - uses: actions/checkout@v3 - - uses: actions/setup-java@v2 + - uses: actions/setup-java@v3 with: distribution: 'zulu' - java-version: 11 + java-version: 17 - uses: actions/cache@v3 with: path: | diff --git a/.github/workflows/deploy-test.yml b/.github/workflows/deploy-test.yml index 20082590d..c4f55e6af 100644 --- a/.github/workflows/deploy-test.yml +++ b/.github/workflows/deploy-test.yml @@ -19,10 +19,10 @@ jobs: environment: app-center steps: - uses: actions/checkout@v3 - - uses: actions/setup-java@v2 + - uses: actions/setup-java@v3 with: distribution: 'zulu' - java-version: 11 + java-version: 17 - uses: actions/cache@v3 with: path: | @@ -89,10 +89,10 @@ jobs: if: github.event_name != 'pull_request_target' steps: - uses: actions/checkout@v3 - - uses: actions/setup-java@v2 + - uses: actions/setup-java@v3 with: distribution: 'zulu' - java-version: 11 + java-version: 17 - uses: actions/cache@v3 with: path: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3def08953..6d50c45d5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,10 +2,12 @@ name: Tests on: push: - branches: [ master, develop ] + branches: + - master + - develop + - 'hotfix/**' tags: [ '*' ] pull_request: - branches: [ master, develop ] jobs: @@ -17,10 +19,10 @@ jobs: - uses: fkirc/skip-duplicate-actions@master - uses: actions/checkout@v3 - uses: gradle/wrapper-validation-action@v1 - - uses: actions/setup-java@v2 + - uses: actions/setup-java@v3 with: distribution: 'zulu' - java-version: 11 + java-version: 17 - uses: actions/cache@v3 with: path: | @@ -43,10 +45,10 @@ jobs: - uses: fkirc/skip-duplicate-actions@master - uses: actions/checkout@v3 - uses: gradle/wrapper-validation-action@v1 - - uses: actions/setup-java@v2 + - uses: actions/setup-java@v3 with: distribution: 'zulu' - java-version: 11 + java-version: 17 - uses: actions/cache@v3 with: path: | @@ -69,10 +71,10 @@ jobs: - uses: fkirc/skip-duplicate-actions@master - uses: actions/checkout@v3 - uses: gradle/wrapper-validation-action@v1 - - uses: actions/setup-java@v2 + - uses: actions/setup-java@v3 with: distribution: 'zulu' - java-version: 11 + java-version: 17 - uses: actions/cache@v3 with: path: | diff --git a/.gitignore b/.gitignore index cd5ff7146..980085e38 100644 --- a/.gitignore +++ b/.gitignore @@ -65,6 +65,8 @@ captures/ .idea/uiDesigner.xml .idea/runConfigurations.xml .idea/discord.xml +.idea/migrations.xml +.idea/androidTestResultsUserPreferences.xml # Keystore files *.jks @@ -119,3 +121,4 @@ Thumbs.db app/src/release/agconnect-services.json app/src/release/agconnect-credentials.json .idea/deploymentTargetDropDown.xml +.idea/kotlinc.xml diff --git a/LICENSE b/LICENSE index c97032f74..a1fc37058 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2022 Wulkanowy + Copyright 2023 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.cs.md b/README.cs.md index 5c1e5ea71..8171b27d6 100644 --- a/README.cs.md +++ b/README.cs.md @@ -1,18 +1,13 @@ -[English version of README](README.en.md) - -[Deutsche Version von README](README.de.md) - -[Polska wersja README](README.md) - -[Slovenská verzia README](README.sk.md) +Česká verze / [Deutsche Version](README.de.md) / [English version](README.en.md) / [Polska wersja](README.md) / [Slovenská verzia](README.sk.md) # Wulkanowy -[![GitHub Workflow Status](https://img.shields.io/github/workflow/status/wulkanowy/wulkanowy/Tests/develop?style=flat-square)](https://github.com/wulkanowy/wulkanowy/actions) +[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/wulkanowy/wulkanowy/test.yml?branch=develop&style=flat-square)](https://github.com/wulkanowy/wulkanowy/actions) [![Codecov](https://img.shields.io/codecov/c/github/wulkanowy/wulkanowy/master.svg?style=flat-square)](https://codecov.io/gh/wulkanowy/wulkanowy) [![Discord](https://img.shields.io/discord/390889354199040011.svg?style=flat-square)](https://discord.gg/vccAQBr) [![F-Droid](https://img.shields.io/f-droid/v/io.github.wulkanowy.svg?style=flat-square)](https://f-droid.org/packages/io.github.wulkanowy/) [![Last release](https://img.shields.io/github/release/wulkanowy/wulkanowy.svg?logo=github&style=flat-square)](https://github.com/wulkanowy/wulkanowy/releases) +[![Crowdin](https://badges.crowdin.net/wulkanowy2/localized.svg)](https://translate.wulkanowy.net.pl) Neoficiální klient deníku VULCAN UONET+ pro žáka a rodiče @@ -39,7 +34,7 @@ Neoficiální klient deníku VULCAN UONET+ pro žáka a rodiče * podpora více účtů s možností přejmenování žáků * tmavý a černý (AMOLED) motiv * offline režim -* žádné reklamy +* volitelné reklamy na podporu projektu ## Stáhnout @@ -57,7 +52,7 @@ Aktuální verzi si můžete stáhnout z Google Play, F-Droid nebo Huawei AppGal Můžete si také stáhnout [vývojovou verzi](https://wulkanowy.github.io/#download), která zahrnuje nové funkce připravované pro příští vydání -## Postaveno s +## Postaveno s pomocí * [Wulkanowy SDK](https://github.com/wulkanowy/sdk) diff --git a/README.de.md b/README.de.md index b9e1d1ec1..972f66ba9 100644 --- a/README.de.md +++ b/README.de.md @@ -1,14 +1,13 @@ -[Polska wersja README](README.md) - -[English version of README](README.en.md) +[Česká verze](README.cs.md) / Deutsche Version / [English version](README.en.md) / [Polska wersja](README.md) / [Slovenská verzia](README.sk.md) # Wulkanowy -[![GitHub Workflow Status](https://img.shields.io/github/workflow/status/wulkanowy/wulkanowy/Tests/develop?style=flat-square)](https://github.com/wulkanowy/wulkanowy/actions) +[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/wulkanowy/wulkanowy/test.yml?branch=develop&style=flat-square)](https://github.com/wulkanowy/wulkanowy/actions) [![Codecov](https://img.shields.io/codecov/c/github/wulkanowy/wulkanowy/master.svg?style=flat-square)](https://codecov.io/gh/wulkanowy/wulkanowy) [![Discord](https://img.shields.io/discord/390889354199040011.svg?style=flat-square)](https://discord.gg/vccAQBr) [![F-Droid](https://img.shields.io/f-droid/v/io.github.wulkanowy.svg?style=flat-square)](https://f-droid.org/packages/io.github.wulkanowy/) [![Last release](https://img.shields.io/github/release/wulkanowy/wulkanowy.svg?logo=github&style=flat-square)](https://github.com/wulkanowy/wulkanowy/releases) +[![Crowdin](https://badges.crowdin.net/wulkanowy2/localized.svg)](https://translate.wulkanowy.net.pl) Inoffizieller Android VULCAN UONET+ Registrierungsclient für Schüler und ihre Eltern @@ -22,7 +21,7 @@ Inoffizieller Android VULCAN UONET+ Registrierungsclient für Schüler und ihre * Prozentsatz der Anwesenheit * Prüfungen * Stundenplan - * Unterricht abgeschlossen + * abgeschlossene Unterrichtsstunden * Nachrichten * Hausaufgaben * Anmerkungen @@ -35,7 +34,7 @@ Inoffizieller Android VULCAN UONET+ Registrierungsclient für Schüler und ihre * Unterstützung für mehrere Konten mit der Möglichkeit, den Namen des Schülers zu ändern * dunkles und schwarzes (AMOLED) Thema * Offline-Modus -* keine Werbung +* optionale Werbungen, die es uns ermöglichen das Projekt zu unterstützen ## Herunterladen diff --git a/README.en.md b/README.en.md index 1ac2a6721..6e4da4637 100644 --- a/README.en.md +++ b/README.en.md @@ -1,18 +1,13 @@ -[Polska wersja README](README.md) - -[Deutsche Version von README](README.de.md) - -[Česká verze README](README.cs.md) - -[Slovenská verzia README](README.sk.md) +[Česká verze](README.cs.md) / [Deutsche Version](README.de.md) / English version / [Polska wersja](README.md) / [Slovenská verzia](README.sk.md) # Wulkanowy -[![GitHub Workflow Status](https://img.shields.io/github/workflow/status/wulkanowy/wulkanowy/Tests/develop?style=flat-square)](https://github.com/wulkanowy/wulkanowy/actions) +[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/wulkanowy/wulkanowy/test.yml?branch=develop&style=flat-square)](https://github.com/wulkanowy/wulkanowy/actions) [![Codecov](https://img.shields.io/codecov/c/github/wulkanowy/wulkanowy/master.svg?style=flat-square)](https://codecov.io/gh/wulkanowy/wulkanowy) [![Discord](https://img.shields.io/discord/390889354199040011.svg?style=flat-square)](https://discord.gg/vccAQBr) [![F-Droid](https://img.shields.io/f-droid/v/io.github.wulkanowy.svg?style=flat-square)](https://f-droid.org/packages/io.github.wulkanowy/) [![Last release](https://img.shields.io/github/release/wulkanowy/wulkanowy.svg?logo=github&style=flat-square)](https://github.com/wulkanowy/wulkanowy/releases) +[![Crowdin](https://badges.crowdin.net/wulkanowy2/localized.svg)](https://translate.wulkanowy.net.pl) Unofficial android VULCAN UONET+ register client for both students and their parents @@ -39,7 +34,7 @@ Unofficial android VULCAN UONET+ register client for both students and their par * support for multiple accounts with the ability to rename students * dark and black (AMOLED) theme * offline mode -* no ads +* optional ads which allow to support the project ## Download diff --git a/README.md b/README.md index e7c7d4c5e..f3d2e29a2 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,13 @@ -[English version of README](README.en.md) - -[Deutsche Version von README](README.de.md) - -[Česká verze README](README.cs.md) - -[Slovenská verzia README](README.sk.md) +[Česká verze](README.cs.md) / [Deutsche Version](README.de.md) / [English version](README.en.md) / Polska wersja / [Slovenská verzia](README.sk.md) # Wulkanowy -[![GitHub Workflow Status](https://img.shields.io/github/workflow/status/wulkanowy/wulkanowy/Tests/develop?style=flat-square)](https://github.com/wulkanowy/wulkanowy/actions) +[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/wulkanowy/wulkanowy/test.yml?branch=develop&style=flat-square)](https://github.com/wulkanowy/wulkanowy/actions) [![Codecov](https://img.shields.io/codecov/c/github/wulkanowy/wulkanowy/master.svg?style=flat-square)](https://codecov.io/gh/wulkanowy/wulkanowy) [![Discord](https://img.shields.io/discord/390889354199040011.svg?style=flat-square)](https://discord.gg/vccAQBr) [![F-Droid](https://img.shields.io/f-droid/v/io.github.wulkanowy.svg?style=flat-square)](https://f-droid.org/packages/io.github.wulkanowy/) [![Last release](https://img.shields.io/github/release/wulkanowy/wulkanowy.svg?logo=github&style=flat-square)](https://github.com/wulkanowy/wulkanowy/releases) +[![Crowdin](https://badges.crowdin.net/wulkanowy2/localized.svg)](https://translate.wulkanowy.net.pl) Nieoficjalny klient dziennika VULCAN UONET+ dla ucznia i rodzica @@ -39,7 +34,7 @@ Nieoficjalny klient dziennika VULCAN UONET+ dla ucznia i rodzica * obsługa wielu kont wraz z możliwością zmiany nazwy ucznia * ciemny i czarny (AMOLED) motyw * tryb offline -* brak reklam +* opcjonalne reklamy umożliwiające wsparcie projektu ## Pobierz diff --git a/README.sk.md b/README.sk.md index 2f3ba41dd..ff0c6e3c9 100644 --- a/README.sk.md +++ b/README.sk.md @@ -1,18 +1,13 @@ -[English version of README](README.en.md) - -[Deutsche Version von README](README.de.md) - -[Polska wersja README](README.md) - -[Česká verze README](README.cs.md) +[Česká verze](README.cs.md) / [Deutsche Version](README.de.md) / [English version](README.en.md) / [Polska wersja](README.md) / Slovenská verzia # Wulkanowy -[![GitHub Workflow Status](https://img.shields.io/github/workflow/status/wulkanowy/wulkanowy/Tests/develop?style=flat-square)](https://github.com/wulkanowy/wulkanowy/actions) +[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/wulkanowy/wulkanowy/test.yml?branch=develop&style=flat-square)](https://github.com/wulkanowy/wulkanowy/actions) [![Codecov](https://img.shields.io/codecov/c/github/wulkanowy/wulkanowy/master.svg?style=flat-square)](https://codecov.io/gh/wulkanowy/wulkanowy) [![Discord](https://img.shields.io/discord/390889354199040011.svg?style=flat-square)](https://discord.gg/vccAQBr) [![F-Droid](https://img.shields.io/f-droid/v/io.github.wulkanowy.svg?style=flat-square)](https://f-droid.org/packages/io.github.wulkanowy/) [![Last release](https://img.shields.io/github/release/wulkanowy/wulkanowy.svg?logo=github&style=flat-square)](https://github.com/wulkanowy/wulkanowy/releases) +[![Crowdin](https://badges.crowdin.net/wulkanowy2/localized.svg)](https://translate.wulkanowy.net.pl) Neoficiálny klient denníka VULCAN UONET+ pre žiaka a rodičov @@ -39,7 +34,7 @@ Neoficiálny klient denníka VULCAN UONET+ pre žiaka a rodičov * podpora viacerých účtov s možnosťou premenovania žiakov * tmavý a čierny (AMOLED) motív * offline režim -* žiadne reklamy +* voliteľné reklamy na podporu projektu ## Stiahnuť @@ -57,7 +52,7 @@ Aktuálnu verziu si môžete stiahnuť z Google Play, F-Droid alebo Huawei AppGa Môžete si tiež stiahnuť [vývojovú verziu](https://wulkanowy.github.io/#download), ktorá zahrňuje nové funkcie pripravované pre budúce vydanie -## Postavené s +## Postavené s pomocou * [Wulkanowy SDK](https://github.com/wulkanowy/sdk) diff --git a/app/build.gradle b/app/build.gradle index 6584b0d28..e6d35f548 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,8 +1,11 @@ +import com.github.triplet.gradle.androidpublisher.ReleaseStatus +import ru.cian.huawei.publish.ReleaseNote + apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlinx-serialization' apply plugin: 'kotlin-parcelize' -apply plugin: 'kotlin-kapt' +apply plugin: 'com.google.devtools.ksp' apply plugin: 'dagger.hilt.android.plugin' apply plugin: 'com.google.gms.google-services' apply plugin: 'com.google.firebase.crashlytics' @@ -10,37 +13,29 @@ apply plugin: 'com.github.triplet.play' apply plugin: 'ru.cian.huawei-publish' apply plugin: 'com.mikepenz.aboutlibraries.plugin' apply plugin: 'com.huawei.agconnect' +apply plugin: 'kotlin-kapt' apply from: 'jacoco.gradle' apply from: 'sonarqube.gradle' apply from: 'hooks.gradle' android { namespace 'io.github.wulkanowy' - compileSdkVersion 32 + compileSdk 34 defaultConfig { applicationId "io.github.wulkanowy" testApplicationId "io.github.tests.wulkanowy" minSdkVersion 21 - targetSdkVersion 32 - versionCode 114 - versionName "1.7.5" + targetSdkVersion 34 + versionCode 147 + versionName "2.4.1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" resValue "string", "app_name", "Wulkanowy" - manifestPlaceholders = [ firebase_enabled: project.hasProperty("enableFirebase"), admob_project_id: "" ] - javaCompileOptions { - annotationProcessorOptions { - arguments += [ - "room.schemaLocation": "$projectDir/schemas".toString(), - "room.incremental" : "true" - ] - } - } buildConfigField "String", "SINGLE_SUPPORT_AD_ID", "null" buildConfigField "String", "DASHBOARD_TILE_AD_ID", "null" @@ -73,6 +68,7 @@ android { proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' signingConfig signingConfigs.release buildConfigField "String", "MESSAGES_BASE_URL", "\"https://messages.wulkanowy.net.pl\"" + buildConfigField "String", "SCHOOLS_BASE_URL", '"https://schools.wulkanowy.net.pl"' } debug { minifyEnabled false @@ -82,10 +78,11 @@ android { versionNameSuffix "-dev" ext.enableCrashlytics = project.hasProperty("enableFirebase") buildConfigField "String", "MESSAGES_BASE_URL", "\"https://messages.wulkanowy.net.pl\"" + buildConfigField "String", "SCHOOLS_BASE_URL", '"https://schools.wulkanowy.net.pl"' } } - flavorDimensions "platform" + flavorDimensions += "platform" productFlavors { hms { @@ -116,6 +113,7 @@ android { buildFeatures { viewBinding true + buildConfig true } bundle { @@ -124,27 +122,29 @@ android { } } - testOptions.unitTests { - includeAndroidResources = true + testOptions { + unitTests.includeAndroidResources = true // workaround HMS test errors https://github.com/robolectric/robolectric/issues/2750 - all { jvmArgs '-noverify' } + unitTests.all { jvmArgs '-noverify' } } compileOptions { coreLibraryDesugaringEnabled true - sourceCompatibility JavaVersion.VERSION_11 - targetCompatibility JavaVersion.VERSION_11 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = "11" + jvmTarget = "17" freeCompilerArgs += ["-opt-in=kotlin.RequiresOptIn", "-Xjvm-default=all"] } packagingOptions { resources { excludes += ['META-INF/library_release.kotlin_module', - 'META-INF/library-core_release.kotlin_module'] + 'META-INF/library-core_release.kotlin_module', + 'META-INF/LICENSE.md', + 'META-INF/LICENSE-notice.md'] } } @@ -156,13 +156,16 @@ android { kapt { correctErrorTypes true } +ksp { + arg("room.schemaLocation", "$projectDir/schemas".toString()) +} play { defaultToAppBundles = false track = 'production' -// releaseStatus = com.github.triplet.gradle.androidpublisher.ReleaseStatus.IN_PROGRESS -// userFraction = 0.05d - updatePriority = 4 + releaseStatus = ReleaseStatus.IN_PROGRESS + userFraction = 0.50d + updatePriority = 1 enabled.set(false) } @@ -171,54 +174,60 @@ huaweiPublish { hmsRelease { credentialsPath = "$rootDir/app/src/release/agconnect-credentials.json" buildFormat = "aab" - deployType = "draft" + deployType = "publish" + releaseNotes = [ + new ReleaseNote( + "pl-PL", + "$projectDir/src/main/play/release-notes/pl-PL/default.txt" + ) + ] } } } ext { - work_manager = "2.7.1" - android_hilt = "1.0.0" - room = "2.4.3" - chucker = "3.5.2" - mockk = "1.12.7" - coroutines = "1.6.4" + work_manager = "2.9.0" + android_hilt = "1.1.0" + room = "2.6.1" + chucker = "4.0.0" + mockk = "1.13.9" + coroutines = "1.7.3" } dependencies { - implementation "io.github.wulkanowy:sdk:1.7.5" + implementation 'io.github.wulkanowy:sdk:2.4.1' - coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.8' + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4' - implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.0" + implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines" - 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.core:core-ktx:1.12.0' + implementation 'androidx.core:core-splashscreen:1.0.1' + implementation "androidx.activity:activity-ktx:1.8.2" + implementation "androidx.appcompat:appcompat:1.6.1" + implementation "androidx.fragment:fragment-ktx:1.6.2" + implementation "androidx.annotation:annotation:1.7.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.preference:preference-ktx:1.2.1" + implementation "androidx.recyclerview:recyclerview:1.3.2" + implementation "androidx.viewpager2:viewpager2:1.1.0-beta02" implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.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.google.android.material:material:1.10.0" implementation "com.github.wulkanowy:material-chips-input:2.3.1" implementation "com.github.PhilJay:MPAndroidChart:v3.1.0" - implementation 'com.github.lopspower:CircularImageView:4.2.0' + implementation 'com.github.lopspower:CircularImageView:4.3.0' - implementation "androidx.work:work-runtime-ktx:$work_manager" + implementation "androidx.work:work-runtime:$work_manager" playImplementation "androidx.work:work-gcm:$work_manager" - implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.5.1" + implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.7.0" implementation "androidx.room:room-runtime:$room" implementation "androidx.room:room-ktx:$room" - kapt "androidx.room:room-compiler:$room" + ksp "androidx.room:room-compiler:$room" implementation "com.google.dagger:hilt-android:$hilt_version" kapt "com.google.dagger:hilt-android-compiler:$hilt_version" @@ -229,32 +238,38 @@ dependencies { implementation "com.github.YarikSOffice:lingver:1.3.0" 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.10.0" + implementation "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0" + implementation "com.squareup.okhttp3:logging-interceptor:4.12.0" + implementation "com.squareup.okhttp3:okhttp-urlconnection:4.12.0" implementation "com.jakewharton.timber:timber:5.0.1" - implementation "at.favre.lib:slf4j-timber:1.0.1" - implementation 'com.github.bastienpaulfr:Treessence:1.0.5' + implementation 'com.github.Faierbel:slf4j-timber:2.0' + implementation 'com.github.bastienpaulfr:Treessence:1.1.2' implementation "com.mikepenz:aboutlibraries-core:$about_libraries" - implementation "io.coil-kt:coil:2.2.0" - implementation "io.github.wulkanowy:AppKillerManager:3.0.0" + implementation 'io.coil-kt:coil:2.5.0' + implementation "io.github.wulkanowy:AppKillerManager:3.0.1" implementation 'me.xdrop:fuzzywuzzy:1.4.0' - implementation 'com.fredporciuncula:flow-preferences:1.8.0' + implementation 'com.fredporciuncula:flow-preferences:1.9.1' + implementation 'org.apache.commons:commons-text:1.11.0' - playImplementation platform('com.google.firebase:firebase-bom:30.3.2') - playImplementation 'com.google.firebase:firebase-analytics-ktx' - playImplementation 'com.google.firebase:firebase-messaging:' + playImplementation platform('com.google.firebase:firebase-bom:32.7.2') + playImplementation 'com.google.firebase:firebase-analytics' + playImplementation 'com.google.firebase:firebase-messaging' playImplementation 'com.google.firebase:firebase-crashlytics:' - 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:21.1.0' + playImplementation 'com.google.firebase:firebase-config' - hmsImplementation 'com.huawei.hms:hianalytics:6.7.0.300' - hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.7.1.300' + playImplementation 'com.google.android.gms:play-services-ads:22.6.0' + playImplementation "com.google.android.play:integrity:1.3.0" + playImplementation 'com.google.android.play:app-update-ktx:2.1.0' + playImplementation 'com.google.android.play:review-ktx:2.0.1' + playImplementation "com.google.android.ump:user-messaging-platform:2.2.0" - releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker" + hmsImplementation 'com.huawei.hms:hianalytics:6.12.0.301' + hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.9.1.303' - debugImplementation "com.github.ChuckerTeam.Chucker:library:$chucker" + 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' @@ -263,17 +278,17 @@ dependencies { testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines" testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version" - 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" + testImplementation 'org.robolectric:robolectric:4.11.1' + testImplementation "androidx.test:runner:1.5.2" + testImplementation "androidx.test.ext:junit:1.1.5" + testImplementation "androidx.test:core:1.5.0" testImplementation "androidx.room:room-testing:$room" testImplementation "com.google.dagger:hilt-android-testing:$hilt_version" kaptTest "com.google.dagger:hilt-android-compiler:$hilt_version" - androidTestImplementation "androidx.test:core:1.4.0" - androidTestImplementation "androidx.test:runner:1.4.0" - androidTestImplementation "androidx.test.ext:junit:1.1.3" + androidTestImplementation "androidx.test:core:1.5.0" + androidTestImplementation "androidx.test:runner:1.5.2" + androidTestImplementation "androidx.test.ext:junit:1.1.5" androidTestImplementation "io.mockk:mockk-android:$mockk" androidTestImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version" } diff --git a/app/jacoco.gradle b/app/jacoco.gradle index f253673e6..67ffdb13b 100644 --- a/app/jacoco.gradle +++ b/app/jacoco.gradle @@ -1,16 +1,16 @@ apply plugin: "jacoco" jacoco { - toolVersion "0.8.7" + toolVersion "0.8.11" reportsDirectory.set(file("$buildDir/reports")) } -tasks.withType(Test) { +tasks.withType(Test).configureEach { jacoco.includeNoLocationClasses = true jacoco.excludes = ['jdk.internal.*'] } -task jacocoTestReport(type: JacocoReport) { +tasks.register('jacocoTestReport', JacocoReport) { group = "Reporting" description = "Generate Jacoco coverage reports" @@ -33,19 +33,19 @@ task jacocoTestReport(type: JacocoReport) { '**/*_Factory.*'] classDirectories.setFrom(fileTree( - dir: "$buildDir/intermediates/classes/debug", - excludes: excludes + dir: "$buildDir/intermediates/classes/debug", + excludes: excludes ) + fileTree( - dir: "$buildDir/tmp/kotlin-classes/fdroidDebug", - excludes: excludes + dir: "$buildDir/tmp/kotlin-classes/fdroidDebug", + excludes: excludes )) sourceDirectories.setFrom(files([ - "src/main/java", - "src/fdroid/java" + "src/main/java", + "src/fdroid/java" ])) executionData.setFrom(fileTree( - dir: project.projectDir, - includes: ["**/*.exec", "**/*.ec"] + dir: project.projectDir, + includes: ["**/*.exec", "**/*.ec"] )) } diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index fd9482613..0fd49f6ac 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -1,5 +1,6 @@ # General -dontobfuscate +-ignorewarnings #Config for wulkanowy @@ -24,3 +25,18 @@ #Config for Material Components -keep class com.google.android.material.tabs.** { *; } + + +#Config for HMS SDK +-keepattributes *Annotation* +-keepattributes Exceptions +-keepattributes InnerClasses +-keepattributes Signature +-keep class com.huawei.agconnect.**{*;} +-keep class com.huawei.hianalytics.**{*;} +-keep class com.huawei.updatesdk.**{*;} +-keep class com.huawei.hms.**{*;} + + +#Config for Wulkanowy SDK +-keep,allowobfuscation,allowshrinking class retrofit2.Response diff --git a/app/schemas/io.github.wulkanowy.data.db.AppDatabase/52.json b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/52.json new file mode 100644 index 000000000..129d1917b --- /dev/null +++ b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/52.json @@ -0,0 +1,2421 @@ +{ + "formatVersion": 1, + "database": { + "version": 52, + "identityHash": "8742176f26afcc81279d4a073dca2949", + "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, `read_by` INTEGER, `unread_by` INTEGER, `has_attachments` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL, `content` TEXT NOT NULL, `sender` TEXT, `recipients` TEXT)", + "fields": [ + { + "fieldPath": "messageGlobalKey", + "columnName": "message_global_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mailboxKey", + "columnName": "mailbox_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "messageId", + "columnName": "message_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "correspondents", + "columnName": "correspondents", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "folderId", + "columnName": "folder_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "readBy", + "columnName": "read_by", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "unreadBy", + "columnName": "unread_by", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hasAttachments", + "columnName": "has_attachments", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sender", + "columnName": "sender", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "recipients", + "columnName": "recipients", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "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, '8742176f26afcc81279d4a073dca2949')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.github.wulkanowy.data.db.AppDatabase/53.json b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/53.json new file mode 100644 index 000000000..985617872 --- /dev/null +++ b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/53.json @@ -0,0 +1,2439 @@ +{ + "formatVersion": 1, + "database": { + "version": 53, + "identityHash": "1dc96a366125ec9f8567da87cdc9c863", + "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}` (`email` TEXT NOT NULL, `message_global_key` TEXT NOT NULL, `mailbox_key` TEXT NOT NULL, `message_id` INTEGER NOT NULL, `correspondents` TEXT NOT NULL, `subject` TEXT NOT NULL, `date` INTEGER NOT NULL, `folder_id` INTEGER NOT NULL, `unread` INTEGER NOT NULL, `read_by` INTEGER, `unread_by` INTEGER, `has_attachments` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL, `content` TEXT NOT NULL, `sender` TEXT, `recipients` TEXT)", + "fields": [ + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "messageGlobalKey", + "columnName": "message_global_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mailboxKey", + "columnName": "mailbox_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "messageId", + "columnName": "message_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "correspondents", + "columnName": "correspondents", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "folderId", + "columnName": "folder_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "readBy", + "columnName": "read_by", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "unreadBy", + "columnName": "unread_by", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hasAttachments", + "columnName": "has_attachments", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sender", + "columnName": "sender", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "recipients", + "columnName": "recipients", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "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, `email` TEXT NOT NULL, `symbol` TEXT NOT NULL, `schoolId` TEXT NOT NULL, `fullName` TEXT NOT NULL, `userName` TEXT NOT NULL, `studentName` TEXT NOT NULL, `schoolNameShort` TEXT NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`globalKey`))", + "fields": [ + { + "fieldPath": "globalKey", + "columnName": "globalKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "symbol", + "columnName": "symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolId", + "columnName": "schoolId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fullName", + "columnName": "fullName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userName", + "columnName": "userName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentName", + "columnName": "studentName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolNameShort", + "columnName": "schoolNameShort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "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, '1dc96a366125ec9f8567da87cdc9c863')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.github.wulkanowy.data.db.AppDatabase/54.json b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/54.json new file mode 100644 index 000000000..7b41672b9 --- /dev/null +++ b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/54.json @@ -0,0 +1,2439 @@ +{ + "formatVersion": 1, + "database": { + "version": 54, + "identityHash": "1dc96a366125ec9f8567da87cdc9c863", + "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}` (`email` TEXT NOT NULL, `message_global_key` TEXT NOT NULL, `mailbox_key` TEXT NOT NULL, `message_id` INTEGER NOT NULL, `correspondents` TEXT NOT NULL, `subject` TEXT NOT NULL, `date` INTEGER NOT NULL, `folder_id` INTEGER NOT NULL, `unread` INTEGER NOT NULL, `read_by` INTEGER, `unread_by` INTEGER, `has_attachments` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL, `content` TEXT NOT NULL, `sender` TEXT, `recipients` TEXT)", + "fields": [ + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "messageGlobalKey", + "columnName": "message_global_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mailboxKey", + "columnName": "mailbox_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "messageId", + "columnName": "message_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "correspondents", + "columnName": "correspondents", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "folderId", + "columnName": "folder_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "readBy", + "columnName": "read_by", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "unreadBy", + "columnName": "unread_by", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hasAttachments", + "columnName": "has_attachments", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sender", + "columnName": "sender", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "recipients", + "columnName": "recipients", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "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, `email` TEXT NOT NULL, `symbol` TEXT NOT NULL, `schoolId` TEXT NOT NULL, `fullName` TEXT NOT NULL, `userName` TEXT NOT NULL, `studentName` TEXT NOT NULL, `schoolNameShort` TEXT NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`globalKey`))", + "fields": [ + { + "fieldPath": "globalKey", + "columnName": "globalKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "symbol", + "columnName": "symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolId", + "columnName": "schoolId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fullName", + "columnName": "fullName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userName", + "columnName": "userName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentName", + "columnName": "studentName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolNameShort", + "columnName": "schoolNameShort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "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, '1dc96a366125ec9f8567da87cdc9c863')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.github.wulkanowy.data.db.AppDatabase/55.json b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/55.json new file mode 100644 index 000000000..60c2efbe0 --- /dev/null +++ b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/55.json @@ -0,0 +1,2435 @@ +{ + "formatVersion": 1, + "database": { + "version": 55, + "identityHash": "cba22eea6d26cf4d6b9a388ba3329a12", + "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}` (`email` TEXT NOT NULL, `message_global_key` TEXT NOT NULL, `mailbox_key` TEXT NOT NULL, `message_id` INTEGER NOT NULL, `correspondents` TEXT NOT NULL, `subject` TEXT NOT NULL, `date` INTEGER NOT NULL, `folder_id` INTEGER NOT NULL, `unread` INTEGER NOT NULL, `read_by` INTEGER, `unread_by` INTEGER, `has_attachments` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL, `content` TEXT NOT NULL, `sender` TEXT, `recipients` TEXT)", + "fields": [ + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "messageGlobalKey", + "columnName": "message_global_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mailboxKey", + "columnName": "mailbox_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "messageId", + "columnName": "message_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "correspondents", + "columnName": "correspondents", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "folderId", + "columnName": "folder_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "readBy", + "columnName": "read_by", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "unreadBy", + "columnName": "unread_by", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hasAttachments", + "columnName": "has_attachments", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sender", + "columnName": "sender", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "recipients", + "columnName": "recipients", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MessageAttachments", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`message_global_key` TEXT NOT NULL, `url` TEXT NOT NULL, `filename` TEXT NOT NULL, PRIMARY KEY(`message_global_key`, `url`, `filename`))", + "fields": [ + { + "fieldPath": "messageGlobalKey", + "columnName": "message_global_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "filename", + "columnName": "filename", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "message_global_key", + "url", + "filename" + ], + "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, `email` TEXT NOT NULL, `symbol` TEXT NOT NULL, `schoolId` TEXT NOT NULL, `fullName` TEXT NOT NULL, `userName` TEXT NOT NULL, `studentName` TEXT NOT NULL, `schoolNameShort` TEXT NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`globalKey`))", + "fields": [ + { + "fieldPath": "globalKey", + "columnName": "globalKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "symbol", + "columnName": "symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolId", + "columnName": "schoolId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fullName", + "columnName": "fullName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userName", + "columnName": "userName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentName", + "columnName": "studentName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolNameShort", + "columnName": "schoolNameShort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "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, 'cba22eea6d26cf4d6b9a388ba3329a12')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.github.wulkanowy.data.db.AppDatabase/56.json b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/56.json new file mode 100644 index 000000000..1a26e717b --- /dev/null +++ b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/56.json @@ -0,0 +1,2442 @@ +{ + "formatVersion": 1, + "database": { + "version": 56, + "identityHash": "48f0538bd21601eb5322a7d850e04134", + "entities": [ + { + "tableName": "Students", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`scrapper_base_url` TEXT NOT NULL, `scrapper_domain_suffix` TEXT NOT NULL DEFAULT '', `mobile_base_url` TEXT NOT NULL, `login_type` TEXT NOT NULL, `login_mode` TEXT NOT NULL, `certificate_key` TEXT NOT NULL, `private_key` TEXT NOT NULL, `is_parent` INTEGER NOT NULL, `email` TEXT NOT NULL, `password` TEXT NOT NULL, `symbol` TEXT NOT NULL, `student_id` INTEGER NOT NULL, `user_login_id` INTEGER NOT NULL, `user_name` TEXT NOT NULL, `student_name` TEXT NOT NULL, `school_id` TEXT NOT NULL, `school_short` TEXT NOT NULL, `school_name` TEXT NOT NULL, `class_name` TEXT NOT NULL, `class_id` INTEGER NOT NULL, `is_current` INTEGER NOT NULL, `registration_date` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `nick` TEXT NOT NULL, `avatar_color` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "scrapperBaseUrl", + "columnName": "scrapper_base_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scrapperDomainSuffix", + "columnName": "scrapper_domain_suffix", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "mobileBaseUrl", + "columnName": "mobile_base_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "loginType", + "columnName": "login_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "loginMode", + "columnName": "login_mode", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "certificateKey", + "columnName": "certificate_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "privateKey", + "columnName": "private_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isParent", + "columnName": "is_parent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "symbol", + "columnName": "symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userLoginId", + "columnName": "user_login_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userName", + "columnName": "user_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentName", + "columnName": "student_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolSymbol", + "columnName": "school_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolShortName", + "columnName": "school_short", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolName", + "columnName": "school_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "className", + "columnName": "class_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isCurrent", + "columnName": "is_current", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "registrationDate", + "columnName": "registration_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "nick", + "columnName": "nick", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatarColor", + "columnName": "avatar_color", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_Students_email_symbol_student_id_school_id_class_id", + "unique": true, + "columnNames": [ + "email", + "symbol", + "student_id", + "school_id", + "class_id" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Students_email_symbol_student_id_school_id_class_id` ON `${TABLE_NAME}` (`email`, `symbol`, `student_id`, `school_id`, `class_id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "Semesters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `kindergarten_diary_id` INTEGER NOT NULL DEFAULT 0, `diary_name` TEXT NOT NULL, `school_year` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `semester_name` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `unit_id` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_current` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "kindergartenDiaryId", + "columnName": "kindergarten_diary_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "diaryName", + "columnName": "diary_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolYear", + "columnName": "school_year", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterName", + "columnName": "semester_name", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unitId", + "columnName": "unit_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "current", + "columnName": "is_current", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_Semesters_student_id_diary_id_kindergarten_diary_id_semester_id", + "unique": true, + "columnNames": [ + "student_id", + "diary_id", + "kindergarten_diary_id", + "semester_id" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Semesters_student_id_diary_id_kindergarten_diary_id_semester_id` ON `${TABLE_NAME}` (`student_id`, `diary_id`, `kindergarten_diary_id`, `semester_id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "Exams", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `entry_date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `group` TEXT NOT NULL, `type` TEXT NOT NULL, `description` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "entryDate", + "columnName": "entry_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Timetable", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `number` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `subjectOld` TEXT NOT NULL, `group` TEXT NOT NULL, `room` TEXT NOT NULL, `roomOld` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacherOld` TEXT NOT NULL, `info` TEXT NOT NULL, `student_plan` INTEGER NOT NULL, `changes` INTEGER NOT NULL, `canceled` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subjectOld", + "columnName": "subjectOld", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "room", + "columnName": "room", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "roomOld", + "columnName": "roomOld", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherOld", + "columnName": "teacherOld", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "info", + "columnName": "info", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isStudentPlan", + "columnName": "student_plan", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "changes", + "columnName": "changes", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canceled", + "columnName": "canceled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Attendance", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `time_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `number` INTEGER NOT NULL, `subject` TEXT NOT NULL, `name` TEXT NOT NULL, `presence` INTEGER NOT NULL, `absence` INTEGER NOT NULL, `exemption` INTEGER NOT NULL, `lateness` INTEGER NOT NULL, `excused` INTEGER NOT NULL, `deleted` INTEGER NOT NULL, `excusable` INTEGER NOT NULL, `excuse_status` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timeId", + "columnName": "time_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "presence", + "columnName": "presence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absence", + "columnName": "absence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exemption", + "columnName": "exemption", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lateness", + "columnName": "lateness", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excused", + "columnName": "excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deleted", + "columnName": "deleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excusable", + "columnName": "excusable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excuseStatus", + "columnName": "excuse_status", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AttendanceSummary", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `subject_id` INTEGER NOT NULL, `month` INTEGER NOT NULL, `presence` INTEGER NOT NULL, `absence` INTEGER NOT NULL, `absence_excused` INTEGER NOT NULL, `absence_for_school_reasons` INTEGER NOT NULL, `lateness` INTEGER NOT NULL, `lateness_excused` INTEGER NOT NULL, `exemption` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subjectId", + "columnName": "subject_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "month", + "columnName": "month", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "presence", + "columnName": "presence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absence", + "columnName": "absence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absenceExcused", + "columnName": "absence_excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absenceForSchoolReasons", + "columnName": "absence_for_school_reasons", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lateness", + "columnName": "lateness", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latenessExcused", + "columnName": "lateness_excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exemption", + "columnName": "exemption", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Grades", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `entry` TEXT NOT NULL, `value` REAL NOT NULL, `modifier` REAL NOT NULL, `comment` TEXT NOT NULL, `color` TEXT NOT NULL, `grade_symbol` TEXT NOT NULL, `description` TEXT NOT NULL, `weight` TEXT NOT NULL, `weightValue` REAL NOT NULL, `date` INTEGER NOT NULL, `teacher` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_read` INTEGER NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "entry", + "columnName": "entry", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "modifier", + "columnName": "modifier", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "comment", + "columnName": "comment", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "gradeSymbol", + "columnName": "grade_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "weightValue", + "columnName": "weightValue", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRead", + "columnName": "is_read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradesSummary", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `position` INTEGER NOT NULL, `subject` TEXT NOT NULL, `predicted_grade` TEXT NOT NULL, `final_grade` TEXT NOT NULL, `proposed_points` TEXT NOT NULL, `final_points` TEXT NOT NULL, `points_sum` TEXT NOT NULL, `average` REAL NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_predicted_grade_notified` INTEGER NOT NULL, `is_final_grade_notified` INTEGER NOT NULL, `predicted_grade_last_change` INTEGER NOT NULL, `final_grade_last_change` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "predictedGrade", + "columnName": "predicted_grade", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "finalGrade", + "columnName": "final_grade", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "proposedPoints", + "columnName": "proposed_points", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "finalPoints", + "columnName": "final_points", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pointsSum", + "columnName": "points_sum", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "average", + "columnName": "average", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPredictedGradeNotified", + "columnName": "is_predicted_grade_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isFinalGradeNotified", + "columnName": "is_final_grade_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "predictedGradeLastChange", + "columnName": "predicted_grade_last_change", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "finalGradeLastChange", + "columnName": "final_grade_last_change", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradePartialStatistics", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `class_average` TEXT NOT NULL, `student_average` TEXT NOT NULL, `class_amounts` TEXT NOT NULL, `student_amounts` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "classAverage", + "columnName": "class_average", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentAverage", + "columnName": "student_average", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "classAmounts", + "columnName": "class_amounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentAmounts", + "columnName": "student_amounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradesPointsStatistics", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `others` REAL NOT NULL, `student` REAL NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "others", + "columnName": "others", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "student", + "columnName": "student", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradeSemesterStatistics", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `amounts` TEXT NOT NULL, `student_grade` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "amounts", + "columnName": "amounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentGrade", + "columnName": "student_grade", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Messages", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`email` TEXT NOT NULL, `message_global_key` TEXT NOT NULL, `mailbox_key` TEXT NOT NULL, `message_id` INTEGER NOT NULL, `correspondents` TEXT NOT NULL, `subject` TEXT NOT NULL, `date` INTEGER NOT NULL, `folder_id` INTEGER NOT NULL, `unread` INTEGER NOT NULL, `read_by` INTEGER, `unread_by` INTEGER, `has_attachments` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL, `content` TEXT NOT NULL, `sender` TEXT, `recipients` TEXT)", + "fields": [ + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "messageGlobalKey", + "columnName": "message_global_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mailboxKey", + "columnName": "mailbox_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "messageId", + "columnName": "message_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "correspondents", + "columnName": "correspondents", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "folderId", + "columnName": "folder_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "readBy", + "columnName": "read_by", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "unreadBy", + "columnName": "unread_by", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hasAttachments", + "columnName": "has_attachments", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sender", + "columnName": "sender", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "recipients", + "columnName": "recipients", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MessageAttachments", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`message_global_key` TEXT NOT NULL, `url` TEXT NOT NULL, `filename` TEXT NOT NULL, PRIMARY KEY(`message_global_key`, `url`, `filename`))", + "fields": [ + { + "fieldPath": "messageGlobalKey", + "columnName": "message_global_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "filename", + "columnName": "filename", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "message_global_key", + "url", + "filename" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Notes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `category` TEXT NOT NULL, `category_type` INTEGER NOT NULL, `is_points_show` INTEGER NOT NULL, `points` INTEGER NOT NULL, `content` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_read` INTEGER NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "category", + "columnName": "category", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "categoryType", + "columnName": "category_type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPointsShow", + "columnName": "is_points_show", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "points", + "columnName": "points", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRead", + "columnName": "is_read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Homework", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `entry_date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `content` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `attachments` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_done` INTEGER NOT NULL, `is_notified` INTEGER NOT NULL, `is_added_by_user` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "entryDate", + "columnName": "entry_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isDone", + "columnName": "is_done", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isAddedByUser", + "columnName": "is_added_by_user", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Subjects", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `real_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "realId", + "columnName": "real_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "LuckyNumbers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `lucky_number` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "luckyNumber", + "columnName": "lucky_number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "CompletedLesson", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `number` INTEGER NOT NULL, `subject` TEXT NOT NULL, `topic` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `substitution` TEXT NOT NULL, `absence` TEXT NOT NULL, `resources` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "topic", + "columnName": "topic", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "substitution", + "columnName": "substitution", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "absence", + "columnName": "absence", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "resources", + "columnName": "resources", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Mailboxes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`globalKey` TEXT NOT NULL, `email` TEXT NOT NULL, `symbol` TEXT NOT NULL, `schoolId` TEXT NOT NULL, `fullName` TEXT NOT NULL, `userName` TEXT NOT NULL, `studentName` TEXT NOT NULL, `schoolNameShort` TEXT NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`globalKey`))", + "fields": [ + { + "fieldPath": "globalKey", + "columnName": "globalKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "symbol", + "columnName": "symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolId", + "columnName": "schoolId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fullName", + "columnName": "fullName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userName", + "columnName": "userName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentName", + "columnName": "studentName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolNameShort", + "columnName": "schoolNameShort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "globalKey" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Recipients", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`mailboxGlobalKey` TEXT NOT NULL, `studentMailboxGlobalKey` TEXT NOT NULL, `fullName` TEXT NOT NULL, `userName` TEXT NOT NULL, `schoolShortName` TEXT NOT NULL, `type` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "mailboxGlobalKey", + "columnName": "mailboxGlobalKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentMailboxGlobalKey", + "columnName": "studentMailboxGlobalKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fullName", + "columnName": "fullName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userName", + "columnName": "userName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolShortName", + "columnName": "schoolShortName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MobileDevices", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_login_id` INTEGER NOT NULL, `device_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `date` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "userLoginId", + "columnName": "user_login_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deviceId", + "columnName": "device_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Teachers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `name` TEXT NOT NULL, `short_name` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shortName", + "columnName": "short_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "School", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `address` TEXT NOT NULL, `contact` TEXT NOT NULL, `headmaster` TEXT NOT NULL, `pedagogue` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contact", + "columnName": "contact", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "headmaster", + "columnName": "headmaster", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pedagogue", + "columnName": "pedagogue", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Conferences", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `title` TEXT NOT NULL, `subject` TEXT NOT NULL, `agenda` TEXT NOT NULL, `present_on_conference` TEXT NOT NULL, `conference_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "agenda", + "columnName": "agenda", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "presentOnConference", + "columnName": "present_on_conference", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "conferenceId", + "columnName": "conference_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimetableAdditional", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `repeat_id` BLOB DEFAULT NULL, `is_added_by_user` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repeatId", + "columnName": "repeat_id", + "affinity": "BLOB", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "isAddedByUser", + "columnName": "is_added_by_user", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "StudentInfo", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `full_name` TEXT NOT NULL, `first_name` TEXT NOT NULL, `second_name` TEXT NOT NULL, `surname` TEXT NOT NULL, `birth_date` INTEGER NOT NULL, `birth_place` TEXT NOT NULL, `gender` TEXT NOT NULL, `has_polish_citizenship` INTEGER NOT NULL, `family_name` TEXT NOT NULL, `parents_names` TEXT NOT NULL, `address` TEXT NOT NULL, `registered_address` TEXT NOT NULL, `correspondence_address` TEXT NOT NULL, `phone_number` TEXT NOT NULL, `cell_phone_number` TEXT NOT NULL, `email` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `first_guardian_full_name` TEXT, `first_guardian_kinship` TEXT, `first_guardian_address` TEXT, `first_guardian_phones` TEXT, `first_guardian_email` TEXT, `second_guardian_full_name` TEXT, `second_guardian_kinship` TEXT, `second_guardian_address` TEXT, `second_guardian_phones` TEXT, `second_guardian_email` TEXT)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fullName", + "columnName": "full_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "firstName", + "columnName": "first_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "secondName", + "columnName": "second_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "surname", + "columnName": "surname", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "birthDate", + "columnName": "birth_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "birthPlace", + "columnName": "birth_place", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "gender", + "columnName": "gender", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasPolishCitizenship", + "columnName": "has_polish_citizenship", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "familyName", + "columnName": "family_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentsNames", + "columnName": "parents_names", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "registeredAddress", + "columnName": "registered_address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "correspondenceAddress", + "columnName": "correspondence_address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "phoneNumber", + "columnName": "phone_number", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cellPhoneNumber", + "columnName": "cell_phone_number", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "firstGuardian.fullName", + "columnName": "first_guardian_full_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.kinship", + "columnName": "first_guardian_kinship", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.address", + "columnName": "first_guardian_address", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.phones", + "columnName": "first_guardian_phones", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.email", + "columnName": "first_guardian_email", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.fullName", + "columnName": "second_guardian_full_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.kinship", + "columnName": "second_guardian_kinship", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.address", + "columnName": "second_guardian_address", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.phones", + "columnName": "second_guardian_phones", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.email", + "columnName": "second_guardian_email", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimetableHeaders", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `content` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "SchoolAnnouncements", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_login_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `content` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "userLoginId", + "columnName": "user_login_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Notifications", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `type` TEXT NOT NULL, `destination` TEXT NOT NULL DEFAULT '{\"type\":\"io.github.wulkanowy.ui.modules.Destination.Dashboard\"}', `date` INTEGER NOT NULL, `data` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "destination", + "columnName": "destination", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'{\"type\":\"io.github.wulkanowy.ui.modules.Destination.Dashboard\"}'" + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AdminMessages", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `version_name` INTEGER, `version_max` INTEGER, `target_register_host` TEXT, `target_flavor` TEXT, `destination_url` TEXT, `priority` TEXT NOT NULL, `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": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '48f0538bd21601eb5322a7d850e04134')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.github.wulkanowy.data.db.AppDatabase/57.json b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/57.json new file mode 100644 index 000000000..2eff12231 --- /dev/null +++ b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/57.json @@ -0,0 +1,2443 @@ +{ + "formatVersion": 1, + "database": { + "version": 57, + "identityHash": "d15dbe7d7e4d7df98ec98d9a3a4b5fcd", + "entities": [ + { + "tableName": "Students", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`scrapper_base_url` TEXT NOT NULL, `scrapper_domain_suffix` TEXT NOT NULL DEFAULT '', `mobile_base_url` TEXT NOT NULL, `login_type` TEXT NOT NULL, `login_mode` TEXT NOT NULL, `certificate_key` TEXT NOT NULL, `private_key` TEXT NOT NULL, `is_parent` INTEGER NOT NULL, `email` TEXT NOT NULL, `password` TEXT NOT NULL, `symbol` TEXT NOT NULL, `student_id` INTEGER NOT NULL, `user_login_id` INTEGER NOT NULL, `user_name` TEXT NOT NULL, `student_name` TEXT NOT NULL, `school_id` TEXT NOT NULL, `school_short` TEXT NOT NULL, `school_name` TEXT NOT NULL, `class_name` TEXT NOT NULL, `class_id` INTEGER NOT NULL, `is_current` INTEGER NOT NULL, `registration_date` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `nick` TEXT NOT NULL, `avatar_color` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "scrapperBaseUrl", + "columnName": "scrapper_base_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scrapperDomainSuffix", + "columnName": "scrapper_domain_suffix", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "mobileBaseUrl", + "columnName": "mobile_base_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "loginType", + "columnName": "login_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "loginMode", + "columnName": "login_mode", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "certificateKey", + "columnName": "certificate_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "privateKey", + "columnName": "private_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isParent", + "columnName": "is_parent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "symbol", + "columnName": "symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userLoginId", + "columnName": "user_login_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userName", + "columnName": "user_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentName", + "columnName": "student_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolSymbol", + "columnName": "school_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolShortName", + "columnName": "school_short", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolName", + "columnName": "school_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "className", + "columnName": "class_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isCurrent", + "columnName": "is_current", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "registrationDate", + "columnName": "registration_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "nick", + "columnName": "nick", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatarColor", + "columnName": "avatar_color", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_Students_email_symbol_student_id_school_id_class_id", + "unique": true, + "columnNames": [ + "email", + "symbol", + "student_id", + "school_id", + "class_id" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Students_email_symbol_student_id_school_id_class_id` ON `${TABLE_NAME}` (`email`, `symbol`, `student_id`, `school_id`, `class_id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "Semesters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `kindergarten_diary_id` INTEGER NOT NULL DEFAULT 0, `diary_name` TEXT NOT NULL, `school_year` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `semester_name` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `unit_id` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_current` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "kindergartenDiaryId", + "columnName": "kindergarten_diary_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "diaryName", + "columnName": "diary_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolYear", + "columnName": "school_year", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterName", + "columnName": "semester_name", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unitId", + "columnName": "unit_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "current", + "columnName": "is_current", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_Semesters_student_id_diary_id_kindergarten_diary_id_semester_id", + "unique": true, + "columnNames": [ + "student_id", + "diary_id", + "kindergarten_diary_id", + "semester_id" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Semesters_student_id_diary_id_kindergarten_diary_id_semester_id` ON `${TABLE_NAME}` (`student_id`, `diary_id`, `kindergarten_diary_id`, `semester_id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "Exams", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `entry_date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `group` TEXT NOT NULL, `type` TEXT NOT NULL, `description` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "entryDate", + "columnName": "entry_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Timetable", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `number` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `subjectOld` TEXT NOT NULL, `group` TEXT NOT NULL, `room` TEXT NOT NULL, `roomOld` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacherOld` TEXT NOT NULL, `info` TEXT NOT NULL, `student_plan` INTEGER NOT NULL, `changes` INTEGER NOT NULL, `canceled` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subjectOld", + "columnName": "subjectOld", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "room", + "columnName": "room", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "roomOld", + "columnName": "roomOld", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherOld", + "columnName": "teacherOld", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "info", + "columnName": "info", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isStudentPlan", + "columnName": "student_plan", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "changes", + "columnName": "changes", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canceled", + "columnName": "canceled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Attendance", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `time_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `number` INTEGER NOT NULL, `subject` TEXT NOT NULL, `name` TEXT NOT NULL, `presence` INTEGER NOT NULL, `absence` INTEGER NOT NULL, `exemption` INTEGER NOT NULL, `lateness` INTEGER NOT NULL, `excused` INTEGER NOT NULL, `deleted` INTEGER NOT NULL, `excusable` INTEGER NOT NULL, `excuse_status` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timeId", + "columnName": "time_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "presence", + "columnName": "presence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absence", + "columnName": "absence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exemption", + "columnName": "exemption", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lateness", + "columnName": "lateness", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excused", + "columnName": "excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deleted", + "columnName": "deleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excusable", + "columnName": "excusable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excuseStatus", + "columnName": "excuse_status", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AttendanceSummary", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `subject_id` INTEGER NOT NULL, `month` INTEGER NOT NULL, `presence` INTEGER NOT NULL, `absence` INTEGER NOT NULL, `absence_excused` INTEGER NOT NULL, `absence_for_school_reasons` INTEGER NOT NULL, `lateness` INTEGER NOT NULL, `lateness_excused` INTEGER NOT NULL, `exemption` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subjectId", + "columnName": "subject_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "month", + "columnName": "month", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "presence", + "columnName": "presence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absence", + "columnName": "absence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absenceExcused", + "columnName": "absence_excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absenceForSchoolReasons", + "columnName": "absence_for_school_reasons", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lateness", + "columnName": "lateness", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latenessExcused", + "columnName": "lateness_excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exemption", + "columnName": "exemption", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Grades", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `entry` TEXT NOT NULL, `value` REAL NOT NULL, `modifier` REAL NOT NULL, `comment` TEXT NOT NULL, `color` TEXT NOT NULL, `grade_symbol` TEXT NOT NULL, `description` TEXT NOT NULL, `weight` TEXT NOT NULL, `weightValue` REAL NOT NULL, `date` INTEGER NOT NULL, `teacher` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_read` INTEGER NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "entry", + "columnName": "entry", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "modifier", + "columnName": "modifier", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "comment", + "columnName": "comment", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "gradeSymbol", + "columnName": "grade_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "weightValue", + "columnName": "weightValue", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRead", + "columnName": "is_read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradesSummary", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `position` INTEGER NOT NULL, `subject` TEXT NOT NULL, `predicted_grade` TEXT NOT NULL, `final_grade` TEXT NOT NULL, `proposed_points` TEXT NOT NULL, `final_points` TEXT NOT NULL, `points_sum` TEXT NOT NULL, `average` REAL NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_predicted_grade_notified` INTEGER NOT NULL, `is_final_grade_notified` INTEGER NOT NULL, `predicted_grade_last_change` INTEGER NOT NULL, `final_grade_last_change` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "predictedGrade", + "columnName": "predicted_grade", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "finalGrade", + "columnName": "final_grade", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "proposedPoints", + "columnName": "proposed_points", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "finalPoints", + "columnName": "final_points", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pointsSum", + "columnName": "points_sum", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "average", + "columnName": "average", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPredictedGradeNotified", + "columnName": "is_predicted_grade_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isFinalGradeNotified", + "columnName": "is_final_grade_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "predictedGradeLastChange", + "columnName": "predicted_grade_last_change", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "finalGradeLastChange", + "columnName": "final_grade_last_change", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradePartialStatistics", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `class_average` TEXT NOT NULL, `student_average` TEXT NOT NULL, `class_amounts` TEXT NOT NULL, `student_amounts` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "classAverage", + "columnName": "class_average", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentAverage", + "columnName": "student_average", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "classAmounts", + "columnName": "class_amounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentAmounts", + "columnName": "student_amounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradesPointsStatistics", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `others` REAL NOT NULL, `student` REAL NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "others", + "columnName": "others", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "student", + "columnName": "student", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradeSemesterStatistics", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `amounts` TEXT NOT NULL, `student_grade` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "amounts", + "columnName": "amounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentGrade", + "columnName": "student_grade", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Messages", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`email` TEXT NOT NULL, `message_global_key` TEXT NOT NULL, `mailbox_key` TEXT NOT NULL, `message_id` INTEGER NOT NULL, `correspondents` TEXT NOT NULL, `subject` TEXT NOT NULL, `date` INTEGER NOT NULL, `folder_id` INTEGER NOT NULL, `unread` INTEGER NOT NULL, `read_by` INTEGER, `unread_by` INTEGER, `has_attachments` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL, `content` TEXT NOT NULL, `sender` TEXT, `recipients` TEXT)", + "fields": [ + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "messageGlobalKey", + "columnName": "message_global_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mailboxKey", + "columnName": "mailbox_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "messageId", + "columnName": "message_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "correspondents", + "columnName": "correspondents", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "folderId", + "columnName": "folder_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "readBy", + "columnName": "read_by", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "unreadBy", + "columnName": "unread_by", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hasAttachments", + "columnName": "has_attachments", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sender", + "columnName": "sender", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "recipients", + "columnName": "recipients", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MessageAttachments", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`message_global_key` TEXT NOT NULL, `url` TEXT NOT NULL, `filename` TEXT NOT NULL, PRIMARY KEY(`message_global_key`, `url`, `filename`))", + "fields": [ + { + "fieldPath": "messageGlobalKey", + "columnName": "message_global_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "filename", + "columnName": "filename", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "message_global_key", + "url", + "filename" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Notes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `category` TEXT NOT NULL, `category_type` INTEGER NOT NULL, `is_points_show` INTEGER NOT NULL, `points` INTEGER NOT NULL, `content` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_read` INTEGER NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "category", + "columnName": "category", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "categoryType", + "columnName": "category_type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPointsShow", + "columnName": "is_points_show", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "points", + "columnName": "points", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRead", + "columnName": "is_read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Homework", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `entry_date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `content` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `attachments` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_done` INTEGER NOT NULL, `is_notified` INTEGER NOT NULL, `is_added_by_user` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "entryDate", + "columnName": "entry_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isDone", + "columnName": "is_done", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isAddedByUser", + "columnName": "is_added_by_user", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Subjects", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `real_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "realId", + "columnName": "real_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "LuckyNumbers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `lucky_number` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "luckyNumber", + "columnName": "lucky_number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "CompletedLesson", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `number` INTEGER NOT NULL, `subject` TEXT NOT NULL, `topic` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `substitution` TEXT NOT NULL, `absence` TEXT NOT NULL, `resources` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "topic", + "columnName": "topic", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "substitution", + "columnName": "substitution", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "absence", + "columnName": "absence", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "resources", + "columnName": "resources", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Mailboxes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`globalKey` TEXT NOT NULL, `email` TEXT NOT NULL, `symbol` TEXT NOT NULL, `schoolId` TEXT NOT NULL, `fullName` TEXT NOT NULL, `userName` TEXT NOT NULL, `studentName` TEXT NOT NULL, `schoolNameShort` TEXT NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`globalKey`))", + "fields": [ + { + "fieldPath": "globalKey", + "columnName": "globalKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "symbol", + "columnName": "symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolId", + "columnName": "schoolId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fullName", + "columnName": "fullName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userName", + "columnName": "userName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentName", + "columnName": "studentName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolNameShort", + "columnName": "schoolNameShort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "globalKey" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Recipients", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`mailboxGlobalKey` TEXT NOT NULL, `studentMailboxGlobalKey` TEXT NOT NULL, `fullName` TEXT NOT NULL, `userName` TEXT NOT NULL, `schoolShortName` TEXT NOT NULL, `type` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "mailboxGlobalKey", + "columnName": "mailboxGlobalKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentMailboxGlobalKey", + "columnName": "studentMailboxGlobalKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fullName", + "columnName": "fullName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userName", + "columnName": "userName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolShortName", + "columnName": "schoolShortName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MobileDevices", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_login_id` INTEGER NOT NULL, `device_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `date` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "userLoginId", + "columnName": "user_login_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deviceId", + "columnName": "device_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Teachers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `name` TEXT NOT NULL, `short_name` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shortName", + "columnName": "short_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "School", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `address` TEXT NOT NULL, `contact` TEXT NOT NULL, `headmaster` TEXT NOT NULL, `pedagogue` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contact", + "columnName": "contact", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "headmaster", + "columnName": "headmaster", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pedagogue", + "columnName": "pedagogue", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Conferences", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `title` TEXT NOT NULL, `subject` TEXT NOT NULL, `agenda` TEXT NOT NULL, `present_on_conference` TEXT NOT NULL, `conference_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "agenda", + "columnName": "agenda", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "presentOnConference", + "columnName": "present_on_conference", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "conferenceId", + "columnName": "conference_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimetableAdditional", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `repeat_id` BLOB DEFAULT NULL, `is_added_by_user` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repeatId", + "columnName": "repeat_id", + "affinity": "BLOB", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "isAddedByUser", + "columnName": "is_added_by_user", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "StudentInfo", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `full_name` TEXT NOT NULL, `first_name` TEXT NOT NULL, `second_name` TEXT NOT NULL, `surname` TEXT NOT NULL, `birth_date` INTEGER NOT NULL, `birth_place` TEXT NOT NULL, `gender` TEXT NOT NULL, `has_polish_citizenship` INTEGER NOT NULL, `family_name` TEXT NOT NULL, `parents_names` TEXT NOT NULL, `address` TEXT NOT NULL, `registered_address` TEXT NOT NULL, `correspondence_address` TEXT NOT NULL, `phone_number` TEXT NOT NULL, `cell_phone_number` TEXT NOT NULL, `email` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `first_guardian_full_name` TEXT, `first_guardian_kinship` TEXT, `first_guardian_address` TEXT, `first_guardian_phones` TEXT, `first_guardian_email` TEXT, `second_guardian_full_name` TEXT, `second_guardian_kinship` TEXT, `second_guardian_address` TEXT, `second_guardian_phones` TEXT, `second_guardian_email` TEXT)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fullName", + "columnName": "full_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "firstName", + "columnName": "first_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "secondName", + "columnName": "second_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "surname", + "columnName": "surname", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "birthDate", + "columnName": "birth_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "birthPlace", + "columnName": "birth_place", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "gender", + "columnName": "gender", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasPolishCitizenship", + "columnName": "has_polish_citizenship", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "familyName", + "columnName": "family_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentsNames", + "columnName": "parents_names", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "registeredAddress", + "columnName": "registered_address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "correspondenceAddress", + "columnName": "correspondence_address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "phoneNumber", + "columnName": "phone_number", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cellPhoneNumber", + "columnName": "cell_phone_number", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "firstGuardian.fullName", + "columnName": "first_guardian_full_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.kinship", + "columnName": "first_guardian_kinship", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.address", + "columnName": "first_guardian_address", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.phones", + "columnName": "first_guardian_phones", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.email", + "columnName": "first_guardian_email", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.fullName", + "columnName": "second_guardian_full_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.kinship", + "columnName": "second_guardian_kinship", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.address", + "columnName": "second_guardian_address", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.phones", + "columnName": "second_guardian_phones", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.email", + "columnName": "second_guardian_email", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimetableHeaders", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `content` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "SchoolAnnouncements", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_login_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `content` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "userLoginId", + "columnName": "user_login_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Notifications", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `type` TEXT NOT NULL, `destination` TEXT NOT NULL DEFAULT '{\"type\":\"io.github.wulkanowy.ui.modules.Destination.Dashboard\"}', `date` INTEGER NOT NULL, `data` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "destination", + "columnName": "destination", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'{\"type\":\"io.github.wulkanowy.ui.modules.Destination.Dashboard\"}'" + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AdminMessages", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `version_name` INTEGER, `version_max` INTEGER, `target_register_host` TEXT, `target_flavor` TEXT, `destination_url` TEXT, `priority` TEXT NOT NULL, `types` TEXT NOT NULL DEFAULT '[]', `is_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": "types", + "columnName": "types", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'[]'" + }, + { + "fieldPath": "isDismissible", + "columnName": "is_dismissible", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd15dbe7d7e4d7df98ec98d9a3a4b5fcd')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.github.wulkanowy.data.db.AppDatabase/58.json b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/58.json new file mode 100644 index 000000000..e6e71229c --- /dev/null +++ b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/58.json @@ -0,0 +1,2451 @@ +{ + "formatVersion": 1, + "database": { + "version": 58, + "identityHash": "cd1d4f8f2b6e3860fbc1de93d4f5ca42", + "entities": [ + { + "tableName": "Students", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`scrapper_base_url` TEXT NOT NULL, `scrapper_domain_suffix` TEXT NOT NULL DEFAULT '', `mobile_base_url` TEXT NOT NULL, `login_type` TEXT NOT NULL, `login_mode` TEXT NOT NULL, `certificate_key` TEXT NOT NULL, `private_key` TEXT NOT NULL, `is_parent` INTEGER NOT NULL, `email` TEXT NOT NULL, `password` TEXT NOT NULL, `symbol` TEXT NOT NULL, `student_id` INTEGER NOT NULL, `user_login_id` INTEGER NOT NULL, `user_name` TEXT NOT NULL, `student_name` TEXT NOT NULL, `school_id` TEXT NOT NULL, `school_short` TEXT NOT NULL, `school_name` TEXT NOT NULL, `class_name` TEXT NOT NULL, `class_id` INTEGER NOT NULL, `is_current` INTEGER NOT NULL, `registration_date` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `nick` TEXT NOT NULL, `avatar_color` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "scrapperBaseUrl", + "columnName": "scrapper_base_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scrapperDomainSuffix", + "columnName": "scrapper_domain_suffix", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "mobileBaseUrl", + "columnName": "mobile_base_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "loginType", + "columnName": "login_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "loginMode", + "columnName": "login_mode", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "certificateKey", + "columnName": "certificate_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "privateKey", + "columnName": "private_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isParent", + "columnName": "is_parent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "symbol", + "columnName": "symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userLoginId", + "columnName": "user_login_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userName", + "columnName": "user_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentName", + "columnName": "student_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolSymbol", + "columnName": "school_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolShortName", + "columnName": "school_short", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolName", + "columnName": "school_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "className", + "columnName": "class_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isCurrent", + "columnName": "is_current", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "registrationDate", + "columnName": "registration_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "nick", + "columnName": "nick", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatarColor", + "columnName": "avatar_color", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_Students_email_symbol_student_id_school_id_class_id", + "unique": true, + "columnNames": [ + "email", + "symbol", + "student_id", + "school_id", + "class_id" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Students_email_symbol_student_id_school_id_class_id` ON `${TABLE_NAME}` (`email`, `symbol`, `student_id`, `school_id`, `class_id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "Semesters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `kindergarten_diary_id` INTEGER NOT NULL DEFAULT 0, `diary_name` TEXT NOT NULL, `school_year` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `semester_name` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `unit_id` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_current` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "kindergartenDiaryId", + "columnName": "kindergarten_diary_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "diaryName", + "columnName": "diary_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolYear", + "columnName": "school_year", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterName", + "columnName": "semester_name", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unitId", + "columnName": "unit_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "current", + "columnName": "is_current", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_Semesters_student_id_diary_id_kindergarten_diary_id_semester_id", + "unique": true, + "columnNames": [ + "student_id", + "diary_id", + "kindergarten_diary_id", + "semester_id" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Semesters_student_id_diary_id_kindergarten_diary_id_semester_id` ON `${TABLE_NAME}` (`student_id`, `diary_id`, `kindergarten_diary_id`, `semester_id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "Exams", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `entry_date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `group` TEXT NOT NULL, `type` TEXT NOT NULL, `description` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "entryDate", + "columnName": "entry_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Timetable", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `number` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `subjectOld` TEXT NOT NULL, `group` TEXT NOT NULL, `room` TEXT NOT NULL, `roomOld` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacherOld` TEXT NOT NULL, `info` TEXT NOT NULL, `student_plan` INTEGER NOT NULL, `changes` INTEGER NOT NULL, `canceled` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subjectOld", + "columnName": "subjectOld", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "room", + "columnName": "room", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "roomOld", + "columnName": "roomOld", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherOld", + "columnName": "teacherOld", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "info", + "columnName": "info", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isStudentPlan", + "columnName": "student_plan", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "changes", + "columnName": "changes", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canceled", + "columnName": "canceled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Attendance", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `time_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `number` INTEGER NOT NULL, `subject` TEXT NOT NULL, `name` TEXT NOT NULL, `presence` INTEGER NOT NULL, `absence` INTEGER NOT NULL, `exemption` INTEGER NOT NULL, `lateness` INTEGER NOT NULL, `excused` INTEGER NOT NULL, `deleted` INTEGER NOT NULL, `excusable` INTEGER NOT NULL, `excuse_status` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timeId", + "columnName": "time_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "presence", + "columnName": "presence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absence", + "columnName": "absence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exemption", + "columnName": "exemption", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lateness", + "columnName": "lateness", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excused", + "columnName": "excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deleted", + "columnName": "deleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excusable", + "columnName": "excusable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excuseStatus", + "columnName": "excuse_status", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AttendanceSummary", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `subject_id` INTEGER NOT NULL, `month` INTEGER NOT NULL, `presence` INTEGER NOT NULL, `absence` INTEGER NOT NULL, `absence_excused` INTEGER NOT NULL, `absence_for_school_reasons` INTEGER NOT NULL, `lateness` INTEGER NOT NULL, `lateness_excused` INTEGER NOT NULL, `exemption` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subjectId", + "columnName": "subject_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "month", + "columnName": "month", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "presence", + "columnName": "presence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absence", + "columnName": "absence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absenceExcused", + "columnName": "absence_excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absenceForSchoolReasons", + "columnName": "absence_for_school_reasons", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lateness", + "columnName": "lateness", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latenessExcused", + "columnName": "lateness_excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exemption", + "columnName": "exemption", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Grades", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `entry` TEXT NOT NULL, `value` REAL NOT NULL, `modifier` REAL NOT NULL, `comment` TEXT NOT NULL, `color` TEXT NOT NULL, `grade_symbol` TEXT NOT NULL, `description` TEXT NOT NULL, `weight` TEXT NOT NULL, `weightValue` REAL NOT NULL, `date` INTEGER NOT NULL, `teacher` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_read` INTEGER NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "entry", + "columnName": "entry", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "modifier", + "columnName": "modifier", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "comment", + "columnName": "comment", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "gradeSymbol", + "columnName": "grade_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "weightValue", + "columnName": "weightValue", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRead", + "columnName": "is_read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradesSummary", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `position` INTEGER NOT NULL, `subject` TEXT NOT NULL, `predicted_grade` TEXT NOT NULL, `final_grade` TEXT NOT NULL, `proposed_points` TEXT NOT NULL, `final_points` TEXT NOT NULL, `points_sum` TEXT NOT NULL, `average` REAL NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_predicted_grade_notified` INTEGER NOT NULL, `is_final_grade_notified` INTEGER NOT NULL, `predicted_grade_last_change` INTEGER NOT NULL, `final_grade_last_change` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "predictedGrade", + "columnName": "predicted_grade", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "finalGrade", + "columnName": "final_grade", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "proposedPoints", + "columnName": "proposed_points", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "finalPoints", + "columnName": "final_points", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pointsSum", + "columnName": "points_sum", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "average", + "columnName": "average", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPredictedGradeNotified", + "columnName": "is_predicted_grade_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isFinalGradeNotified", + "columnName": "is_final_grade_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "predictedGradeLastChange", + "columnName": "predicted_grade_last_change", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "finalGradeLastChange", + "columnName": "final_grade_last_change", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradePartialStatistics", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `class_average` TEXT NOT NULL, `student_average` TEXT NOT NULL, `class_amounts` TEXT NOT NULL, `student_amounts` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "classAverage", + "columnName": "class_average", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentAverage", + "columnName": "student_average", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "classAmounts", + "columnName": "class_amounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentAmounts", + "columnName": "student_amounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradesPointsStatistics", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `others` REAL NOT NULL, `student` REAL NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "others", + "columnName": "others", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "student", + "columnName": "student", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradeSemesterStatistics", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `amounts` TEXT NOT NULL, `student_grade` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "amounts", + "columnName": "amounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentGrade", + "columnName": "student_grade", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Messages", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`email` TEXT NOT NULL, `message_global_key` TEXT NOT NULL, `mailbox_key` TEXT NOT NULL, `message_id` INTEGER NOT NULL, `correspondents` TEXT NOT NULL, `subject` TEXT NOT NULL, `date` INTEGER NOT NULL, `folder_id` INTEGER NOT NULL, `unread` INTEGER NOT NULL, `read_by` INTEGER, `unread_by` INTEGER, `has_attachments` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL, `content` TEXT NOT NULL, `sender` TEXT, `recipients` TEXT)", + "fields": [ + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "messageGlobalKey", + "columnName": "message_global_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mailboxKey", + "columnName": "mailbox_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "messageId", + "columnName": "message_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "correspondents", + "columnName": "correspondents", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "folderId", + "columnName": "folder_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "readBy", + "columnName": "read_by", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "unreadBy", + "columnName": "unread_by", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hasAttachments", + "columnName": "has_attachments", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sender", + "columnName": "sender", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "recipients", + "columnName": "recipients", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MessageAttachments", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`message_global_key` TEXT NOT NULL, `url` TEXT NOT NULL, `filename` TEXT NOT NULL, PRIMARY KEY(`message_global_key`, `url`, `filename`))", + "fields": [ + { + "fieldPath": "messageGlobalKey", + "columnName": "message_global_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "filename", + "columnName": "filename", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "message_global_key", + "url", + "filename" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Notes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `category` TEXT NOT NULL, `category_type` INTEGER NOT NULL, `is_points_show` INTEGER NOT NULL, `points` INTEGER NOT NULL, `content` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_read` INTEGER NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "category", + "columnName": "category", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "categoryType", + "columnName": "category_type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPointsShow", + "columnName": "is_points_show", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "points", + "columnName": "points", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRead", + "columnName": "is_read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Homework", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `entry_date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `content` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `attachments` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_done` INTEGER NOT NULL, `is_notified` INTEGER NOT NULL, `is_added_by_user` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "entryDate", + "columnName": "entry_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isDone", + "columnName": "is_done", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isAddedByUser", + "columnName": "is_added_by_user", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Subjects", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `real_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "realId", + "columnName": "real_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "LuckyNumbers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `lucky_number` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "luckyNumber", + "columnName": "lucky_number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "CompletedLesson", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `number` INTEGER NOT NULL, `subject` TEXT NOT NULL, `topic` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `substitution` TEXT NOT NULL, `absence` TEXT NOT NULL, `resources` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "topic", + "columnName": "topic", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "substitution", + "columnName": "substitution", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "absence", + "columnName": "absence", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "resources", + "columnName": "resources", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Mailboxes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`globalKey` TEXT NOT NULL, `email` TEXT NOT NULL, `symbol` TEXT NOT NULL, `schoolId` TEXT NOT NULL, `fullName` TEXT NOT NULL, `userName` TEXT NOT NULL, `studentName` TEXT NOT NULL, `schoolNameShort` TEXT NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`globalKey`))", + "fields": [ + { + "fieldPath": "globalKey", + "columnName": "globalKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "symbol", + "columnName": "symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolId", + "columnName": "schoolId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fullName", + "columnName": "fullName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userName", + "columnName": "userName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentName", + "columnName": "studentName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolNameShort", + "columnName": "schoolNameShort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "globalKey" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Recipients", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`mailboxGlobalKey` TEXT NOT NULL, `studentMailboxGlobalKey` TEXT NOT NULL, `fullName` TEXT NOT NULL, `userName` TEXT NOT NULL, `schoolShortName` TEXT NOT NULL, `type` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "mailboxGlobalKey", + "columnName": "mailboxGlobalKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentMailboxGlobalKey", + "columnName": "studentMailboxGlobalKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fullName", + "columnName": "fullName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userName", + "columnName": "userName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolShortName", + "columnName": "schoolShortName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MobileDevices", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_login_id` INTEGER NOT NULL, `device_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `date` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "userLoginId", + "columnName": "user_login_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deviceId", + "columnName": "device_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Teachers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `name` TEXT NOT NULL, `short_name` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shortName", + "columnName": "short_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "School", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `address` TEXT NOT NULL, `contact` TEXT NOT NULL, `headmaster` TEXT NOT NULL, `pedagogue` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contact", + "columnName": "contact", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "headmaster", + "columnName": "headmaster", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pedagogue", + "columnName": "pedagogue", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Conferences", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `title` TEXT NOT NULL, `subject` TEXT NOT NULL, `agenda` TEXT NOT NULL, `present_on_conference` TEXT NOT NULL, `conference_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "agenda", + "columnName": "agenda", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "presentOnConference", + "columnName": "present_on_conference", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "conferenceId", + "columnName": "conference_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimetableAdditional", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `repeat_id` BLOB DEFAULT NULL, `is_added_by_user` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repeatId", + "columnName": "repeat_id", + "affinity": "BLOB", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "isAddedByUser", + "columnName": "is_added_by_user", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "StudentInfo", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `full_name` TEXT NOT NULL, `first_name` TEXT NOT NULL, `second_name` TEXT NOT NULL, `surname` TEXT NOT NULL, `birth_date` INTEGER NOT NULL, `birth_place` TEXT NOT NULL, `gender` TEXT NOT NULL, `has_polish_citizenship` INTEGER NOT NULL, `family_name` TEXT NOT NULL, `parents_names` TEXT NOT NULL, `address` TEXT NOT NULL, `registered_address` TEXT NOT NULL, `correspondence_address` TEXT NOT NULL, `phone_number` TEXT NOT NULL, `cell_phone_number` TEXT NOT NULL, `email` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `first_guardian_full_name` TEXT, `first_guardian_kinship` TEXT, `first_guardian_address` TEXT, `first_guardian_phones` TEXT, `first_guardian_email` TEXT, `second_guardian_full_name` TEXT, `second_guardian_kinship` TEXT, `second_guardian_address` TEXT, `second_guardian_phones` TEXT, `second_guardian_email` TEXT)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fullName", + "columnName": "full_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "firstName", + "columnName": "first_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "secondName", + "columnName": "second_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "surname", + "columnName": "surname", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "birthDate", + "columnName": "birth_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "birthPlace", + "columnName": "birth_place", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "gender", + "columnName": "gender", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasPolishCitizenship", + "columnName": "has_polish_citizenship", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "familyName", + "columnName": "family_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentsNames", + "columnName": "parents_names", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "registeredAddress", + "columnName": "registered_address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "correspondenceAddress", + "columnName": "correspondence_address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "phoneNumber", + "columnName": "phone_number", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cellPhoneNumber", + "columnName": "cell_phone_number", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "firstGuardian.fullName", + "columnName": "first_guardian_full_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.kinship", + "columnName": "first_guardian_kinship", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.address", + "columnName": "first_guardian_address", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.phones", + "columnName": "first_guardian_phones", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.email", + "columnName": "first_guardian_email", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.fullName", + "columnName": "second_guardian_full_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.kinship", + "columnName": "second_guardian_kinship", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.address", + "columnName": "second_guardian_address", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.phones", + "columnName": "second_guardian_phones", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.email", + "columnName": "second_guardian_email", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimetableHeaders", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `content` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "SchoolAnnouncements", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_login_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `content` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "userLoginId", + "columnName": "user_login_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Notifications", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `type` TEXT NOT NULL, `destination` TEXT NOT NULL DEFAULT '{\"type\":\"io.github.wulkanowy.ui.modules.Destination.Dashboard\"}', `date` INTEGER NOT NULL, `data` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "destination", + "columnName": "destination", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'{\"type\":\"io.github.wulkanowy.ui.modules.Destination.Dashboard\"}'" + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AdminMessages", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `version_name` INTEGER, `version_max` INTEGER, `target_register_host` TEXT, `target_flavor` TEXT, `destination_url` TEXT, `priority` TEXT NOT NULL, `types` TEXT NOT NULL DEFAULT '[]', `is_ok_visible` INTEGER NOT NULL DEFAULT 0, `is_x_visible` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "versionMin", + "columnName": "version_name", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "versionMax", + "columnName": "version_max", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "targetRegisterHost", + "columnName": "target_register_host", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "targetFlavor", + "columnName": "target_flavor", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "destinationUrl", + "columnName": "destination_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "priority", + "columnName": "priority", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "types", + "columnName": "types", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'[]'" + }, + { + "fieldPath": "isOkVisible", + "columnName": "is_ok_visible", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isXVisible", + "columnName": "is_x_visible", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'cd1d4f8f2b6e3860fbc1de93d4f5ca42')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.github.wulkanowy.data.db.AppDatabase/59.json b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/59.json new file mode 100644 index 000000000..a3f2e0dc6 --- /dev/null +++ b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/59.json @@ -0,0 +1,2501 @@ +{ + "formatVersion": 1, + "database": { + "version": 59, + "identityHash": "3bd95e40b587e8131a2a2c23aee538c1", + "entities": [ + { + "tableName": "Students", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`scrapper_base_url` TEXT NOT NULL, `scrapper_domain_suffix` TEXT NOT NULL DEFAULT '', `mobile_base_url` TEXT NOT NULL, `login_type` TEXT NOT NULL, `login_mode` TEXT NOT NULL, `certificate_key` TEXT NOT NULL, `private_key` TEXT NOT NULL, `is_parent` INTEGER NOT NULL, `email` TEXT NOT NULL, `password` TEXT NOT NULL, `symbol` TEXT NOT NULL, `student_id` INTEGER NOT NULL, `user_login_id` INTEGER NOT NULL, `user_name` TEXT NOT NULL, `student_name` TEXT NOT NULL, `school_id` TEXT NOT NULL, `school_short` TEXT NOT NULL, `school_name` TEXT NOT NULL, `class_name` TEXT NOT NULL, `class_id` INTEGER NOT NULL, `is_current` INTEGER NOT NULL, `registration_date` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `nick` TEXT NOT NULL, `avatar_color` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "scrapperBaseUrl", + "columnName": "scrapper_base_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scrapperDomainSuffix", + "columnName": "scrapper_domain_suffix", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "mobileBaseUrl", + "columnName": "mobile_base_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "loginType", + "columnName": "login_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "loginMode", + "columnName": "login_mode", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "certificateKey", + "columnName": "certificate_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "privateKey", + "columnName": "private_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isParent", + "columnName": "is_parent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "symbol", + "columnName": "symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userLoginId", + "columnName": "user_login_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userName", + "columnName": "user_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentName", + "columnName": "student_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolSymbol", + "columnName": "school_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolShortName", + "columnName": "school_short", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolName", + "columnName": "school_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "className", + "columnName": "class_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isCurrent", + "columnName": "is_current", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "registrationDate", + "columnName": "registration_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "nick", + "columnName": "nick", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatarColor", + "columnName": "avatar_color", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_Students_email_symbol_student_id_school_id_class_id", + "unique": true, + "columnNames": [ + "email", + "symbol", + "student_id", + "school_id", + "class_id" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Students_email_symbol_student_id_school_id_class_id` ON `${TABLE_NAME}` (`email`, `symbol`, `student_id`, `school_id`, `class_id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "Semesters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `kindergarten_diary_id` INTEGER NOT NULL DEFAULT 0, `diary_name` TEXT NOT NULL, `school_year` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `semester_name` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `unit_id` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_current` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "kindergartenDiaryId", + "columnName": "kindergarten_diary_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "diaryName", + "columnName": "diary_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolYear", + "columnName": "school_year", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterName", + "columnName": "semester_name", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unitId", + "columnName": "unit_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "current", + "columnName": "is_current", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_Semesters_student_id_diary_id_kindergarten_diary_id_semester_id", + "unique": true, + "columnNames": [ + "student_id", + "diary_id", + "kindergarten_diary_id", + "semester_id" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Semesters_student_id_diary_id_kindergarten_diary_id_semester_id` ON `${TABLE_NAME}` (`student_id`, `diary_id`, `kindergarten_diary_id`, `semester_id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "Exams", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `entry_date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `group` TEXT NOT NULL, `type` TEXT NOT NULL, `description` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "entryDate", + "columnName": "entry_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Timetable", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `number` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `subjectOld` TEXT NOT NULL, `group` TEXT NOT NULL, `room` TEXT NOT NULL, `roomOld` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacherOld` TEXT NOT NULL, `info` TEXT NOT NULL, `student_plan` INTEGER NOT NULL, `changes` INTEGER NOT NULL, `canceled` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subjectOld", + "columnName": "subjectOld", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "room", + "columnName": "room", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "roomOld", + "columnName": "roomOld", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherOld", + "columnName": "teacherOld", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "info", + "columnName": "info", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isStudentPlan", + "columnName": "student_plan", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "changes", + "columnName": "changes", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canceled", + "columnName": "canceled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Attendance", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `time_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `number` INTEGER NOT NULL, `subject` TEXT NOT NULL, `name` TEXT NOT NULL, `presence` INTEGER NOT NULL, `absence` INTEGER NOT NULL, `exemption` INTEGER NOT NULL, `lateness` INTEGER NOT NULL, `excused` INTEGER NOT NULL, `deleted` INTEGER NOT NULL, `excusable` INTEGER NOT NULL, `excuse_status` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timeId", + "columnName": "time_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "presence", + "columnName": "presence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absence", + "columnName": "absence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exemption", + "columnName": "exemption", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lateness", + "columnName": "lateness", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excused", + "columnName": "excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deleted", + "columnName": "deleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excusable", + "columnName": "excusable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excuseStatus", + "columnName": "excuse_status", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AttendanceSummary", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `subject_id` INTEGER NOT NULL, `month` INTEGER NOT NULL, `presence` INTEGER NOT NULL, `absence` INTEGER NOT NULL, `absence_excused` INTEGER NOT NULL, `absence_for_school_reasons` INTEGER NOT NULL, `lateness` INTEGER NOT NULL, `lateness_excused` INTEGER NOT NULL, `exemption` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subjectId", + "columnName": "subject_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "month", + "columnName": "month", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "presence", + "columnName": "presence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absence", + "columnName": "absence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absenceExcused", + "columnName": "absence_excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absenceForSchoolReasons", + "columnName": "absence_for_school_reasons", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lateness", + "columnName": "lateness", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latenessExcused", + "columnName": "lateness_excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exemption", + "columnName": "exemption", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Grades", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `entry` TEXT NOT NULL, `value` REAL NOT NULL, `modifier` REAL NOT NULL, `comment` TEXT NOT NULL, `color` TEXT NOT NULL, `grade_symbol` TEXT NOT NULL, `description` TEXT NOT NULL, `weight` TEXT NOT NULL, `weightValue` REAL NOT NULL, `date` INTEGER NOT NULL, `teacher` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_read` INTEGER NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "entry", + "columnName": "entry", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "modifier", + "columnName": "modifier", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "comment", + "columnName": "comment", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "gradeSymbol", + "columnName": "grade_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "weightValue", + "columnName": "weightValue", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRead", + "columnName": "is_read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradesSummary", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `position` INTEGER NOT NULL, `subject` TEXT NOT NULL, `predicted_grade` TEXT NOT NULL, `final_grade` TEXT NOT NULL, `proposed_points` TEXT NOT NULL, `final_points` TEXT NOT NULL, `points_sum` TEXT NOT NULL, `average` REAL NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_predicted_grade_notified` INTEGER NOT NULL, `is_final_grade_notified` INTEGER NOT NULL, `predicted_grade_last_change` INTEGER NOT NULL, `final_grade_last_change` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "predictedGrade", + "columnName": "predicted_grade", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "finalGrade", + "columnName": "final_grade", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "proposedPoints", + "columnName": "proposed_points", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "finalPoints", + "columnName": "final_points", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pointsSum", + "columnName": "points_sum", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "average", + "columnName": "average", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPredictedGradeNotified", + "columnName": "is_predicted_grade_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isFinalGradeNotified", + "columnName": "is_final_grade_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "predictedGradeLastChange", + "columnName": "predicted_grade_last_change", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "finalGradeLastChange", + "columnName": "final_grade_last_change", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradePartialStatistics", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `class_average` TEXT NOT NULL, `student_average` TEXT NOT NULL, `class_amounts` TEXT NOT NULL, `student_amounts` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "classAverage", + "columnName": "class_average", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentAverage", + "columnName": "student_average", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "classAmounts", + "columnName": "class_amounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentAmounts", + "columnName": "student_amounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradesPointsStatistics", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `others` REAL NOT NULL, `student` REAL NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "others", + "columnName": "others", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "student", + "columnName": "student", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradeSemesterStatistics", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `amounts` TEXT NOT NULL, `student_grade` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "amounts", + "columnName": "amounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentGrade", + "columnName": "student_grade", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Messages", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`email` TEXT NOT NULL, `message_global_key` TEXT NOT NULL, `mailbox_key` TEXT NOT NULL, `message_id` INTEGER NOT NULL, `correspondents` TEXT NOT NULL, `subject` TEXT NOT NULL, `date` INTEGER NOT NULL, `folder_id` INTEGER NOT NULL, `unread` INTEGER NOT NULL, `read_by` INTEGER, `unread_by` INTEGER, `has_attachments` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL, `content` TEXT NOT NULL, `sender` TEXT, `recipients` TEXT)", + "fields": [ + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "messageGlobalKey", + "columnName": "message_global_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mailboxKey", + "columnName": "mailbox_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "messageId", + "columnName": "message_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "correspondents", + "columnName": "correspondents", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "folderId", + "columnName": "folder_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "readBy", + "columnName": "read_by", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "unreadBy", + "columnName": "unread_by", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hasAttachments", + "columnName": "has_attachments", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sender", + "columnName": "sender", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "recipients", + "columnName": "recipients", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MessageAttachments", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`message_global_key` TEXT NOT NULL, `url` TEXT NOT NULL, `filename` TEXT NOT NULL, PRIMARY KEY(`message_global_key`, `url`, `filename`))", + "fields": [ + { + "fieldPath": "messageGlobalKey", + "columnName": "message_global_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "filename", + "columnName": "filename", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "message_global_key", + "url", + "filename" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Notes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `category` TEXT NOT NULL, `category_type` INTEGER NOT NULL, `is_points_show` INTEGER NOT NULL, `points` INTEGER NOT NULL, `content` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_read` INTEGER NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "category", + "columnName": "category", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "categoryType", + "columnName": "category_type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPointsShow", + "columnName": "is_points_show", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "points", + "columnName": "points", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRead", + "columnName": "is_read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Homework", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `entry_date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `content` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `attachments` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_done` INTEGER NOT NULL, `is_notified` INTEGER NOT NULL, `is_added_by_user` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "entryDate", + "columnName": "entry_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isDone", + "columnName": "is_done", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isAddedByUser", + "columnName": "is_added_by_user", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Subjects", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `real_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "realId", + "columnName": "real_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "LuckyNumbers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `lucky_number` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "luckyNumber", + "columnName": "lucky_number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "CompletedLesson", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `number` INTEGER NOT NULL, `subject` TEXT NOT NULL, `topic` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `substitution` TEXT NOT NULL, `absence` TEXT NOT NULL, `resources` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "topic", + "columnName": "topic", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "substitution", + "columnName": "substitution", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "absence", + "columnName": "absence", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "resources", + "columnName": "resources", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Mailboxes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`globalKey` TEXT NOT NULL, `email` TEXT NOT NULL, `symbol` TEXT NOT NULL, `schoolId` TEXT NOT NULL, `fullName` TEXT NOT NULL, `userName` TEXT NOT NULL, `studentName` TEXT NOT NULL, `schoolNameShort` TEXT NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`globalKey`))", + "fields": [ + { + "fieldPath": "globalKey", + "columnName": "globalKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "symbol", + "columnName": "symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolId", + "columnName": "schoolId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fullName", + "columnName": "fullName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userName", + "columnName": "userName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentName", + "columnName": "studentName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolNameShort", + "columnName": "schoolNameShort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "globalKey" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Recipients", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`mailboxGlobalKey` TEXT NOT NULL, `studentMailboxGlobalKey` TEXT NOT NULL, `fullName` TEXT NOT NULL, `userName` TEXT NOT NULL, `schoolShortName` TEXT NOT NULL, `type` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "mailboxGlobalKey", + "columnName": "mailboxGlobalKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentMailboxGlobalKey", + "columnName": "studentMailboxGlobalKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fullName", + "columnName": "fullName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userName", + "columnName": "userName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolShortName", + "columnName": "schoolShortName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MobileDevices", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_login_id` INTEGER NOT NULL, `device_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `date` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "userLoginId", + "columnName": "user_login_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deviceId", + "columnName": "device_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Teachers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `name` TEXT NOT NULL, `short_name` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shortName", + "columnName": "short_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "School", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `address` TEXT NOT NULL, `contact` TEXT NOT NULL, `headmaster` TEXT NOT NULL, `pedagogue` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contact", + "columnName": "contact", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "headmaster", + "columnName": "headmaster", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pedagogue", + "columnName": "pedagogue", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Conferences", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `title` TEXT NOT NULL, `subject` TEXT NOT NULL, `agenda` TEXT NOT NULL, `present_on_conference` TEXT NOT NULL, `conference_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "agenda", + "columnName": "agenda", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "presentOnConference", + "columnName": "present_on_conference", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "conferenceId", + "columnName": "conference_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimetableAdditional", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `repeat_id` BLOB DEFAULT NULL, `is_added_by_user` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repeatId", + "columnName": "repeat_id", + "affinity": "BLOB", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "isAddedByUser", + "columnName": "is_added_by_user", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "StudentInfo", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `full_name` TEXT NOT NULL, `first_name` TEXT NOT NULL, `second_name` TEXT NOT NULL, `surname` TEXT NOT NULL, `birth_date` INTEGER NOT NULL, `birth_place` TEXT NOT NULL, `gender` TEXT NOT NULL, `has_polish_citizenship` INTEGER NOT NULL, `family_name` TEXT NOT NULL, `parents_names` TEXT NOT NULL, `address` TEXT NOT NULL, `registered_address` TEXT NOT NULL, `correspondence_address` TEXT NOT NULL, `phone_number` TEXT NOT NULL, `cell_phone_number` TEXT NOT NULL, `email` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `first_guardian_full_name` TEXT, `first_guardian_kinship` TEXT, `first_guardian_address` TEXT, `first_guardian_phones` TEXT, `first_guardian_email` TEXT, `second_guardian_full_name` TEXT, `second_guardian_kinship` TEXT, `second_guardian_address` TEXT, `second_guardian_phones` TEXT, `second_guardian_email` TEXT)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fullName", + "columnName": "full_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "firstName", + "columnName": "first_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "secondName", + "columnName": "second_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "surname", + "columnName": "surname", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "birthDate", + "columnName": "birth_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "birthPlace", + "columnName": "birth_place", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "gender", + "columnName": "gender", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasPolishCitizenship", + "columnName": "has_polish_citizenship", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "familyName", + "columnName": "family_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentsNames", + "columnName": "parents_names", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "registeredAddress", + "columnName": "registered_address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "correspondenceAddress", + "columnName": "correspondence_address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "phoneNumber", + "columnName": "phone_number", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cellPhoneNumber", + "columnName": "cell_phone_number", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "firstGuardian.fullName", + "columnName": "first_guardian_full_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.kinship", + "columnName": "first_guardian_kinship", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.address", + "columnName": "first_guardian_address", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.phones", + "columnName": "first_guardian_phones", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.email", + "columnName": "first_guardian_email", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.fullName", + "columnName": "second_guardian_full_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.kinship", + "columnName": "second_guardian_kinship", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.address", + "columnName": "second_guardian_address", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.phones", + "columnName": "second_guardian_phones", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.email", + "columnName": "second_guardian_email", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimetableHeaders", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `content` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "SchoolAnnouncements", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_login_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `content` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "userLoginId", + "columnName": "user_login_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Notifications", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `type` TEXT NOT NULL, `destination` TEXT NOT NULL DEFAULT '{\"type\":\"io.github.wulkanowy.ui.modules.Destination.Dashboard\"}', `date` INTEGER NOT NULL, `data` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "destination", + "columnName": "destination", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'{\"type\":\"io.github.wulkanowy.ui.modules.Destination.Dashboard\"}'" + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AdminMessages", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `version_name` INTEGER, `version_max` INTEGER, `target_register_host` TEXT, `target_flavor` TEXT, `destination_url` TEXT, `priority` TEXT NOT NULL, `types` TEXT NOT NULL DEFAULT '[]', `is_ok_visible` INTEGER NOT NULL DEFAULT 0, `is_x_visible` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "versionMin", + "columnName": "version_name", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "versionMax", + "columnName": "version_max", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "targetRegisterHost", + "columnName": "target_register_host", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "targetFlavor", + "columnName": "target_flavor", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "destinationUrl", + "columnName": "destination_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "priority", + "columnName": "priority", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "types", + "columnName": "types", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'[]'" + }, + { + "fieldPath": "isOkVisible", + "columnName": "is_ok_visible", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isXVisible", + "columnName": "is_x_visible", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradesDescriptive", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `description` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '3bd95e40b587e8131a2a2c23aee538c1')" + ] + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/io/github/wulkanowy/utils/security/ScramblerTest.kt b/app/src/androidTest/java/io/github/wulkanowy/utils/security/ScramblerTest.kt index 0c47e6bb6..1b0319f69 100644 --- a/app/src/androidTest/java/io/github/wulkanowy/utils/security/ScramblerTest.kt +++ b/app/src/androidTest/java/io/github/wulkanowy/utils/security/ScramblerTest.kt @@ -14,34 +14,37 @@ import kotlin.test.assertFailsWith @RunWith(AndroidJUnit4::class) class ScramblerTest { + private val scrambler = Scrambler(ApplicationProvider.getApplicationContext()) + @Test fun encryptDecryptTest() { - assertEquals("TEST", decrypt(encrypt("TEST", - ApplicationProvider.getApplicationContext()))) + assertEquals( + "TEST", scrambler.decrypt(scrambler.encrypt("TEST")) + ) } @Test fun emptyTextEncryptTest() { assertFailsWith { - decrypt("") + scrambler.decrypt("") } assertFailsWith { - encrypt("", ApplicationProvider.getApplicationContext()) + scrambler.encrypt("") } } @Test @SdkSuppress(minSdkVersion = 18) fun emptyKeyStoreTest() { - val text = encrypt("test", ApplicationProvider.getApplicationContext()) + val text = scrambler.encrypt("test") val keyStore = KeyStore.getInstance("AndroidKeyStore") keyStore.load(null) keyStore.deleteEntry("wulkanowy_password") assertFailsWith { - decrypt(text) + scrambler.decrypt(text) } } } diff --git a/app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml index 7dbec2cb9..9c21d49d9 100644 --- a/app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml @@ -1,5 +1,6 @@ - + - \ No newline at end of file + + diff --git a/app/src/debug/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/debug/res/mipmap-anydpi-v26/ic_launcher_round.xml deleted file mode 100644 index 7dbec2cb9..000000000 --- a/app/src/debug/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/debug/res/mipmap-hdpi/ic_launcher_round.png b/app/src/debug/res/mipmap-hdpi/ic_launcher_round.png deleted file mode 100644 index 81e723ecc..000000000 Binary files a/app/src/debug/res/mipmap-hdpi/ic_launcher_round.png and /dev/null differ diff --git a/app/src/debug/res/mipmap-mdpi/ic_launcher_round.png b/app/src/debug/res/mipmap-mdpi/ic_launcher_round.png deleted file mode 100644 index 394b57076..000000000 Binary files a/app/src/debug/res/mipmap-mdpi/ic_launcher_round.png and /dev/null differ diff --git a/app/src/debug/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/debug/res/mipmap-xhdpi/ic_launcher_round.png deleted file mode 100644 index 365b4d663..000000000 Binary files a/app/src/debug/res/mipmap-xhdpi/ic_launcher_round.png and /dev/null differ diff --git a/app/src/debug/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/debug/res/mipmap-xxhdpi/ic_launcher_round.png deleted file mode 100644 index 463c089b3..000000000 Binary files a/app/src/debug/res/mipmap-xxhdpi/ic_launcher_round.png and /dev/null differ diff --git a/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_round.png deleted file mode 100644 index 53d6f5bbd..000000000 Binary files a/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_round.png and /dev/null differ diff --git a/app/src/fdroid/java/io/github/wulkanowy/utils/AdsHelper.kt b/app/src/fdroid/java/io/github/wulkanowy/utils/AdsHelper.kt index 461d29951..3a3b5948f 100644 --- a/app/src/fdroid/java/io/github/wulkanowy/utils/AdsHelper.kt +++ b/app/src/fdroid/java/io/github/wulkanowy/utils/AdsHelper.kt @@ -5,6 +5,7 @@ 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 kotlinx.coroutines.flow.MutableStateFlow import javax.inject.Inject @Suppress("unused") @@ -13,9 +14,11 @@ class AdsHelper @Inject constructor( private val preferencesRepository: PreferencesRepository ) { + val isMobileAdsSdkInitialized = MutableStateFlow(false) + val canShowAd = false + fun initialize() { preferencesRepository.isAdsEnabled = false - preferencesRepository.isAgreeToProcessData = false preferencesRepository.selectedDashboardTiles -= DashboardItem.Tile.ADS } diff --git a/app/src/fdroid/java/io/github/wulkanowy/utils/AnalyticsHelper.kt b/app/src/fdroid/java/io/github/wulkanowy/utils/AnalyticsHelper.kt index 3bf7e1693..a3eed484a 100644 --- a/app/src/fdroid/java/io/github/wulkanowy/utils/AnalyticsHelper.kt +++ b/app/src/fdroid/java/io/github/wulkanowy/utils/AnalyticsHelper.kt @@ -8,15 +8,7 @@ import javax.inject.Singleton @Suppress("UNUSED_PARAMETER") class AnalyticsHelper @Inject constructor() { - fun logEvent(name: String, vararg params: Pair) { - // do nothing - } - - fun setCurrentScreen(activity: Activity, name: String?) { - // do nothing - } - - fun popCurrentScreen(name: String?) { - // do nothing - } + fun logEvent(name: String, vararg params: Pair) = Unit + fun setCurrentScreen(activity: Activity, name: String?) = Unit + fun popCurrentScreen(name: String?) = Unit } diff --git a/app/src/fdroid/java/io/github/wulkanowy/utils/InAppUpdateHelper.kt b/app/src/fdroid/java/io/github/wulkanowy/utils/InAppUpdateHelper.kt new file mode 100644 index 000000000..51b22ec74 --- /dev/null +++ b/app/src/fdroid/java/io/github/wulkanowy/utils/InAppUpdateHelper.kt @@ -0,0 +1,13 @@ +package io.github.wulkanowy.utils + +import android.view.View +import javax.inject.Inject + +class InAppUpdateHelper @Inject constructor() { + + lateinit var messageContainer: View + + fun checkAndInstallUpdates() {} + + fun onResume() {} +} diff --git a/app/src/fdroid/java/io/github/wulkanowy/utils/IntegrityHelper.kt b/app/src/fdroid/java/io/github/wulkanowy/utils/IntegrityHelper.kt new file mode 100644 index 000000000..7af68058c --- /dev/null +++ b/app/src/fdroid/java/io/github/wulkanowy/utils/IntegrityHelper.kt @@ -0,0 +1,11 @@ +package io.github.wulkanowy.utils + +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class IntegrityHelper @Inject constructor() { + + @Suppress("UNUSED_PARAMETER") + fun getIntegrityToken(requestId: String): String? = null +} diff --git a/app/src/fdroid/java/io/github/wulkanowy/utils/RemoteConfigHelper.kt b/app/src/fdroid/java/io/github/wulkanowy/utils/RemoteConfigHelper.kt new file mode 100644 index 000000000..88f2598f8 --- /dev/null +++ b/app/src/fdroid/java/io/github/wulkanowy/utils/RemoteConfigHelper.kt @@ -0,0 +1,7 @@ +package io.github.wulkanowy.utils + +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class RemoteConfigHelper @Inject constructor() : BaseRemoteConfigHelper() diff --git a/app/src/fdroid/java/io/github/wulkanowy/utils/UpdateHelper.kt b/app/src/fdroid/java/io/github/wulkanowy/utils/UpdateHelper.kt deleted file mode 100644 index 3abab9629..000000000 --- a/app/src/fdroid/java/io/github/wulkanowy/utils/UpdateHelper.kt +++ /dev/null @@ -1,17 +0,0 @@ -package io.github.wulkanowy.utils - -import android.app.Activity -import android.view.View -import javax.inject.Inject - -@Suppress("UNUSED_PARAMETER") -class UpdateHelper @Inject constructor() { - - lateinit var messageContainer: View - - fun checkAndInstallUpdates(activity: Activity) {} - - fun onActivityResult(requestCode: Int, resultCode: Int) {} - - fun onResume(activity: Activity) {} -} diff --git a/app/src/hms/java/io/github/wulkanowy/utils/AdsHelper.kt b/app/src/hms/java/io/github/wulkanowy/utils/AdsHelper.kt index 0e9227022..165a6204f 100644 --- a/app/src/hms/java/io/github/wulkanowy/utils/AdsHelper.kt +++ b/app/src/hms/java/io/github/wulkanowy/utils/AdsHelper.kt @@ -5,6 +5,7 @@ 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 kotlinx.coroutines.flow.MutableStateFlow import javax.inject.Inject @Suppress("unused") @@ -12,10 +13,11 @@ class AdsHelper @Inject constructor( @ApplicationContext private val context: Context, private val preferencesRepository: PreferencesRepository ) { + val isMobileAdsSdkInitialized = MutableStateFlow(false) + val canShowAd = false fun initialize() { preferencesRepository.isAdsEnabled = false - preferencesRepository.isAgreeToProcessData = false preferencesRepository.selectedDashboardTiles -= DashboardItem.Tile.ADS } diff --git a/app/src/hms/java/io/github/wulkanowy/utils/AnalyticsHelper.kt b/app/src/hms/java/io/github/wulkanowy/utils/AnalyticsHelper.kt index 5d33825f1..1f78931ae 100644 --- a/app/src/hms/java/io/github/wulkanowy/utils/AnalyticsHelper.kt +++ b/app/src/hms/java/io/github/wulkanowy/utils/AnalyticsHelper.kt @@ -3,26 +3,38 @@ package io.github.wulkanowy.utils import android.app.Activity import android.content.Context import android.os.Bundle +import com.huawei.agconnect.crash.AGConnectCrash import com.huawei.hms.analytics.HiAnalytics import dagger.hilt.android.qualifiers.ApplicationContext +import io.github.wulkanowy.data.repositories.PreferencesRepository import javax.inject.Inject import javax.inject.Singleton @Singleton class AnalyticsHelper @Inject constructor( - @ApplicationContext private val context: Context + @ApplicationContext private val context: Context, + preferencesRepository: PreferencesRepository, + appInfo: AppInfo, ) { private val analytics by lazy { HiAnalytics.getInstance(context) } + private val connectCrash by lazy { AGConnectCrash.getInstance() } + + init { + if (!appInfo.isDebug) { + connectCrash.setUserId(preferencesRepository.installationId) + } + } + fun logEvent(name: String, vararg params: Pair) { Bundle().apply { - params.forEach { - if (it.second == null) return@forEach - when (it.second) { - is String, is String? -> putString(it.first, it.second as String) - is Int, is Int? -> putInt(it.first, it.second as Int) - is Boolean, is Boolean? -> putBoolean(it.first, it.second as Boolean) + params.forEach { (key, value) -> + if (value == null) return@forEach + when (value) { + is String -> putString(key, value) + is Int -> putInt(key, value) + is Boolean -> putBoolean(key, value) } } analytics.onEvent(name, this) diff --git a/app/src/hms/java/io/github/wulkanowy/utils/CrashLogUtils.kt b/app/src/hms/java/io/github/wulkanowy/utils/CrashLogUtils.kt index b0c34f413..2b1f1d30e 100644 --- a/app/src/hms/java/io/github/wulkanowy/utils/CrashLogUtils.kt +++ b/app/src/hms/java/io/github/wulkanowy/utils/CrashLogUtils.kt @@ -2,7 +2,8 @@ package io.github.wulkanowy.utils import android.util.Log import com.huawei.agconnect.crash.AGConnectCrash -import fr.bipi.tressence.base.FormatterPriorityTree +import fr.bipi.treessence.base.FormatterPriorityTree +import fr.bipi.treessence.common.StackTraceRecorder class CrashLogTree : FormatterPriorityTree(Log.VERBOSE) { @@ -22,16 +23,10 @@ class CrashLogExceptionTree : FormatterPriorityTree(Log.ERROR, ExceptionFilter) override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { if (skipLog(priority, tag, message, t)) return - // Disabled due to a bug in the Huawei library - - /*connectCrash.setCustomKey("priority", priority) - connectCrash.setCustomKey("tag", tag.orEmpty()) - connectCrash.setCustomKey("message", message) - if (t != null) { connectCrash.recordException(t) } else { connectCrash.recordException(StackTraceRecorder(format(priority, tag, message))) - }*/ + } } } diff --git a/app/src/hms/java/io/github/wulkanowy/utils/InAppUpdateHelper.kt b/app/src/hms/java/io/github/wulkanowy/utils/InAppUpdateHelper.kt new file mode 100644 index 000000000..51b22ec74 --- /dev/null +++ b/app/src/hms/java/io/github/wulkanowy/utils/InAppUpdateHelper.kt @@ -0,0 +1,13 @@ +package io.github.wulkanowy.utils + +import android.view.View +import javax.inject.Inject + +class InAppUpdateHelper @Inject constructor() { + + lateinit var messageContainer: View + + fun checkAndInstallUpdates() {} + + fun onResume() {} +} diff --git a/app/src/hms/java/io/github/wulkanowy/utils/IntegrityHelper.kt b/app/src/hms/java/io/github/wulkanowy/utils/IntegrityHelper.kt new file mode 100644 index 000000000..7af68058c --- /dev/null +++ b/app/src/hms/java/io/github/wulkanowy/utils/IntegrityHelper.kt @@ -0,0 +1,11 @@ +package io.github.wulkanowy.utils + +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class IntegrityHelper @Inject constructor() { + + @Suppress("UNUSED_PARAMETER") + fun getIntegrityToken(requestId: String): String? = null +} diff --git a/app/src/hms/java/io/github/wulkanowy/utils/RemoteConfigHelper.kt b/app/src/hms/java/io/github/wulkanowy/utils/RemoteConfigHelper.kt new file mode 100644 index 000000000..88f2598f8 --- /dev/null +++ b/app/src/hms/java/io/github/wulkanowy/utils/RemoteConfigHelper.kt @@ -0,0 +1,7 @@ +package io.github.wulkanowy.utils + +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class RemoteConfigHelper @Inject constructor() : BaseRemoteConfigHelper() diff --git a/app/src/hms/java/io/github/wulkanowy/utils/UpdateHelper.kt b/app/src/hms/java/io/github/wulkanowy/utils/UpdateHelper.kt deleted file mode 100644 index 3abab9629..000000000 --- a/app/src/hms/java/io/github/wulkanowy/utils/UpdateHelper.kt +++ /dev/null @@ -1,17 +0,0 @@ -package io.github.wulkanowy.utils - -import android.app.Activity -import android.view.View -import javax.inject.Inject - -@Suppress("UNUSED_PARAMETER") -class UpdateHelper @Inject constructor() { - - lateinit var messageContainer: View - - fun checkAndInstallUpdates(activity: Activity) {} - - fun onActivityResult(requestCode: Int, resultCode: Int) {} - - fun onResume(activity: Activity) {} -} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7835db902..f43dfdd2c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -8,7 +8,8 @@ - + + @@ -36,13 +37,15 @@ + android:resizeableActivity="true" + tools:ignore="DataExtractionRules,UnusedAttribute"> Resource.mapData(block: (T) -> U) = when (this) { 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.Loading -> "started" is Resource.Success -> "success" + if (showData) " (data: `${it.data}`)" else "" is Resource.Error -> "exception occurred: ${it.error}" } @@ -131,7 +141,7 @@ inline fun networkBoundResource( query().map { Resource.Success(filterResult(it)) } } catch (throwable: Throwable) { onFetchFailed(throwable) - query().map { Resource.Error(throwable) } + flowOf(Resource.Error(throwable)) } } else { query().map { Resource.Success(filterResult(it)) } @@ -148,7 +158,7 @@ inline fun networkBoundResource( crossinline saveFetchResult: suspend (old: ResultType, new: RequestType) -> Unit, crossinline onFetchFailed: (Throwable) -> Unit = { }, crossinline shouldFetch: (ResultType) -> Boolean = { true }, - crossinline mapResult: (ResultType) -> T + crossinline mapResult: (ResultType) -> T, ) = flow { emit(Resource.Loading()) @@ -165,7 +175,7 @@ inline fun networkBoundResource( query().map { Resource.Success(mapResult(it)) } } catch (throwable: Throwable) { onFetchFailed(throwable) - query().map { Resource.Error(throwable) } + flowOf(Resource.Error(throwable)) } } else { query().map { Resource.Success(mapResult(it)) } diff --git a/app/src/main/java/io/github/wulkanowy/data/api/SchoolsService.kt b/app/src/main/java/io/github/wulkanowy/data/api/SchoolsService.kt new file mode 100644 index 000000000..a7da9b63c --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/api/SchoolsService.kt @@ -0,0 +1,14 @@ +package io.github.wulkanowy.data.api + +import io.github.wulkanowy.data.pojos.IntegrityRequest +import io.github.wulkanowy.data.pojos.LoginEvent +import retrofit2.http.Body +import retrofit2.http.POST +import javax.inject.Singleton + +@Singleton +interface SchoolsService { + + @POST("/log/loginEvent") + suspend fun logLoginEvent(@Body request: IntegrityRequest) +} 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 15b38805b..8e5841fe7 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt @@ -1,11 +1,126 @@ package io.github.wulkanowy.data.db import android.content.Context -import androidx.room.* +import androidx.room.AutoMigration +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase import androidx.room.RoomDatabase.JournalMode.TRUNCATE -import io.github.wulkanowy.data.db.dao.* -import io.github.wulkanowy.data.db.entities.* -import io.github.wulkanowy.data.db.migrations.* +import androidx.room.TypeConverters +import io.github.wulkanowy.data.db.dao.AdminMessageDao +import io.github.wulkanowy.data.db.dao.AttendanceDao +import io.github.wulkanowy.data.db.dao.AttendanceSummaryDao +import io.github.wulkanowy.data.db.dao.CompletedLessonsDao +import io.github.wulkanowy.data.db.dao.ConferenceDao +import io.github.wulkanowy.data.db.dao.ExamDao +import io.github.wulkanowy.data.db.dao.GradeDao +import io.github.wulkanowy.data.db.dao.GradeDescriptiveDao +import io.github.wulkanowy.data.db.dao.GradePartialStatisticsDao +import io.github.wulkanowy.data.db.dao.GradePointsStatisticsDao +import io.github.wulkanowy.data.db.dao.GradeSemesterStatisticsDao +import io.github.wulkanowy.data.db.dao.GradeSummaryDao +import io.github.wulkanowy.data.db.dao.HomeworkDao +import io.github.wulkanowy.data.db.dao.LuckyNumberDao +import io.github.wulkanowy.data.db.dao.MailboxDao +import io.github.wulkanowy.data.db.dao.MessageAttachmentDao +import io.github.wulkanowy.data.db.dao.MessagesDao +import io.github.wulkanowy.data.db.dao.MobileDeviceDao +import io.github.wulkanowy.data.db.dao.NoteDao +import io.github.wulkanowy.data.db.dao.NotificationDao +import io.github.wulkanowy.data.db.dao.RecipientDao +import io.github.wulkanowy.data.db.dao.SchoolAnnouncementDao +import io.github.wulkanowy.data.db.dao.SchoolDao +import io.github.wulkanowy.data.db.dao.SemesterDao +import io.github.wulkanowy.data.db.dao.StudentDao +import io.github.wulkanowy.data.db.dao.StudentInfoDao +import io.github.wulkanowy.data.db.dao.SubjectDao +import io.github.wulkanowy.data.db.dao.TeacherDao +import io.github.wulkanowy.data.db.dao.TimetableAdditionalDao +import io.github.wulkanowy.data.db.dao.TimetableDao +import io.github.wulkanowy.data.db.dao.TimetableHeaderDao +import io.github.wulkanowy.data.db.entities.AdminMessage +import io.github.wulkanowy.data.db.entities.Attendance +import io.github.wulkanowy.data.db.entities.AttendanceSummary +import io.github.wulkanowy.data.db.entities.CompletedLesson +import io.github.wulkanowy.data.db.entities.Conference +import io.github.wulkanowy.data.db.entities.Exam +import io.github.wulkanowy.data.db.entities.Grade +import io.github.wulkanowy.data.db.entities.GradeDescriptive +import io.github.wulkanowy.data.db.entities.GradePartialStatistics +import io.github.wulkanowy.data.db.entities.GradePointsStatistics +import io.github.wulkanowy.data.db.entities.GradeSemesterStatistics +import io.github.wulkanowy.data.db.entities.GradeSummary +import io.github.wulkanowy.data.db.entities.Homework +import io.github.wulkanowy.data.db.entities.LuckyNumber +import io.github.wulkanowy.data.db.entities.Mailbox +import io.github.wulkanowy.data.db.entities.Message +import io.github.wulkanowy.data.db.entities.MessageAttachment +import io.github.wulkanowy.data.db.entities.MobileDevice +import io.github.wulkanowy.data.db.entities.Note +import io.github.wulkanowy.data.db.entities.Notification +import io.github.wulkanowy.data.db.entities.Recipient +import io.github.wulkanowy.data.db.entities.School +import io.github.wulkanowy.data.db.entities.SchoolAnnouncement +import io.github.wulkanowy.data.db.entities.Semester +import io.github.wulkanowy.data.db.entities.Student +import io.github.wulkanowy.data.db.entities.StudentInfo +import io.github.wulkanowy.data.db.entities.Subject +import io.github.wulkanowy.data.db.entities.Teacher +import io.github.wulkanowy.data.db.entities.Timetable +import io.github.wulkanowy.data.db.entities.TimetableAdditional +import io.github.wulkanowy.data.db.entities.TimetableHeader +import io.github.wulkanowy.data.db.migrations.Migration10 +import io.github.wulkanowy.data.db.migrations.Migration11 +import io.github.wulkanowy.data.db.migrations.Migration12 +import io.github.wulkanowy.data.db.migrations.Migration13 +import io.github.wulkanowy.data.db.migrations.Migration14 +import io.github.wulkanowy.data.db.migrations.Migration15 +import io.github.wulkanowy.data.db.migrations.Migration16 +import io.github.wulkanowy.data.db.migrations.Migration17 +import io.github.wulkanowy.data.db.migrations.Migration18 +import io.github.wulkanowy.data.db.migrations.Migration19 +import io.github.wulkanowy.data.db.migrations.Migration2 +import io.github.wulkanowy.data.db.migrations.Migration20 +import io.github.wulkanowy.data.db.migrations.Migration21 +import io.github.wulkanowy.data.db.migrations.Migration22 +import io.github.wulkanowy.data.db.migrations.Migration23 +import io.github.wulkanowy.data.db.migrations.Migration24 +import io.github.wulkanowy.data.db.migrations.Migration25 +import io.github.wulkanowy.data.db.migrations.Migration26 +import io.github.wulkanowy.data.db.migrations.Migration27 +import io.github.wulkanowy.data.db.migrations.Migration28 +import io.github.wulkanowy.data.db.migrations.Migration29 +import io.github.wulkanowy.data.db.migrations.Migration3 +import io.github.wulkanowy.data.db.migrations.Migration30 +import io.github.wulkanowy.data.db.migrations.Migration31 +import io.github.wulkanowy.data.db.migrations.Migration32 +import io.github.wulkanowy.data.db.migrations.Migration33 +import io.github.wulkanowy.data.db.migrations.Migration34 +import io.github.wulkanowy.data.db.migrations.Migration35 +import io.github.wulkanowy.data.db.migrations.Migration36 +import io.github.wulkanowy.data.db.migrations.Migration37 +import io.github.wulkanowy.data.db.migrations.Migration38 +import io.github.wulkanowy.data.db.migrations.Migration39 +import io.github.wulkanowy.data.db.migrations.Migration4 +import io.github.wulkanowy.data.db.migrations.Migration40 +import io.github.wulkanowy.data.db.migrations.Migration41 +import io.github.wulkanowy.data.db.migrations.Migration42 +import io.github.wulkanowy.data.db.migrations.Migration43 +import io.github.wulkanowy.data.db.migrations.Migration44 +import io.github.wulkanowy.data.db.migrations.Migration46 +import io.github.wulkanowy.data.db.migrations.Migration49 +import io.github.wulkanowy.data.db.migrations.Migration5 +import io.github.wulkanowy.data.db.migrations.Migration50 +import io.github.wulkanowy.data.db.migrations.Migration51 +import io.github.wulkanowy.data.db.migrations.Migration53 +import io.github.wulkanowy.data.db.migrations.Migration54 +import io.github.wulkanowy.data.db.migrations.Migration55 +import io.github.wulkanowy.data.db.migrations.Migration57 +import io.github.wulkanowy.data.db.migrations.Migration58 +import io.github.wulkanowy.data.db.migrations.Migration6 +import io.github.wulkanowy.data.db.migrations.Migration7 +import io.github.wulkanowy.data.db.migrations.Migration8 +import io.github.wulkanowy.data.db.migrations.Migration9 import io.github.wulkanowy.utils.AppInfo import javax.inject.Singleton @@ -41,12 +156,19 @@ import javax.inject.Singleton TimetableHeader::class, SchoolAnnouncement::class, Notification::class, - AdminMessage::class + AdminMessage::class, + GradeDescriptive::class, ], autoMigrations = [ AutoMigration(from = 44, to = 45), AutoMigration(from = 46, to = 47), AutoMigration(from = 47, to = 48), + AutoMigration(from = 51, to = 52), + AutoMigration(from = 54, to = 55, spec = Migration55::class), + AutoMigration(from = 55, to = 56), + AutoMigration(from = 56, to = 57, spec = Migration57::class), + AutoMigration(from = 57, to = 58, spec = Migration58::class), + AutoMigration(from = 58, to = 59), ], version = AppDatabase.VERSION_SCHEMA, exportSchema = true @@ -55,7 +177,7 @@ import javax.inject.Singleton abstract class AppDatabase : RoomDatabase() { companion object { - const val VERSION_SCHEMA = 51 + const val VERSION_SCHEMA = 59 fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf( Migration2(), @@ -105,6 +227,8 @@ abstract class AppDatabase : RoomDatabase() { Migration49(), Migration50(), Migration51(), + Migration53(), + Migration54(), ) fun newInstance( @@ -178,4 +302,6 @@ abstract class AppDatabase : RoomDatabase() { abstract val notificationDao: NotificationDao abstract val adminMessagesDao: AdminMessageDao + + abstract val gradeDescriptiveDao: GradeDescriptiveDao } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/Converters.kt b/app/src/main/java/io/github/wulkanowy/data/db/Converters.kt index 9d3beae1f..7bc8d12a2 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,6 +1,7 @@ package io.github.wulkanowy.data.db import androidx.room.TypeConverter +import io.github.wulkanowy.data.enums.MessageType import io.github.wulkanowy.ui.modules.Destination import io.github.wulkanowy.utils.toTimestamp import kotlinx.serialization.SerializationException @@ -68,4 +69,9 @@ class Converters { @TypeConverter fun stringToDestination(destination: String): Destination = json.decodeFromString(destination) + @TypeConverter + fun messageTypesToString(types: List): String = json.encodeToString(types) + + @TypeConverter + fun stringToMessageTypes(text: String): List = json.decodeFromString(text) } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/dao/AdminMessageDao.kt b/app/src/main/java/io/github/wulkanowy/data/db/dao/AdminMessageDao.kt index 87f4812da..2b4cb5975 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/dao/AdminMessageDao.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/dao/AdminMessageDao.kt @@ -22,4 +22,4 @@ abstract class AdminMessageDao : BaseDao { deleteAll(oldMessages) insertAll(newMessages) } -} \ No newline at end of file +} diff --git a/app/src/main/java/io/github/wulkanowy/data/db/dao/GradeDescriptiveDao.kt b/app/src/main/java/io/github/wulkanowy/data/db/dao/GradeDescriptiveDao.kt new file mode 100644 index 000000000..6282c0804 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/db/dao/GradeDescriptiveDao.kt @@ -0,0 +1,15 @@ +package io.github.wulkanowy.data.db.dao + +import androidx.room.Dao +import androidx.room.Query +import io.github.wulkanowy.data.db.entities.GradeDescriptive +import kotlinx.coroutines.flow.Flow +import javax.inject.Singleton + +@Singleton +@Dao +interface GradeDescriptiveDao : BaseDao { + + @Query("SELECT * FROM GradesDescriptive WHERE semester_id = :semesterId AND student_id = :studentId") + fun loadAll(semesterId: Int, studentId: Int): Flow> +} diff --git a/app/src/main/java/io/github/wulkanowy/data/db/dao/MailboxDao.kt b/app/src/main/java/io/github/wulkanowy/data/db/dao/MailboxDao.kt index c44ecd0c2..084192a07 100644 --- 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 @@ -3,12 +3,16 @@ package io.github.wulkanowy.data.db.dao import androidx.room.Dao import androidx.room.Query import io.github.wulkanowy.data.db.entities.Mailbox +import kotlinx.coroutines.flow.Flow import javax.inject.Singleton @Singleton @Dao interface MailboxDao : BaseDao { - @Query("SELECT * FROM Mailboxes WHERE userLoginId = :userLoginId ") - suspend fun loadAll(userLoginId: Int): List + @Query("SELECT * FROM Mailboxes WHERE email = :email") + suspend fun loadAll(email: String): List + + @Query("SELECT * FROM Mailboxes WHERE email = :email AND symbol = :symbol AND schoolId = :schoolId") + fun loadAll(email: String, symbol: String, schoolId: String): Flow> } 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 8c730c9bc..1709f7636 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 @@ -16,4 +16,7 @@ interface MessagesDao : BaseDao { @Query("SELECT * FROM Messages WHERE mailbox_key = :mailboxKey AND folder_id = :folder ORDER BY date DESC") fun loadAll(mailboxKey: String, folder: Int): Flow> + + @Query("SELECT * FROM Messages WHERE email = :email AND folder_id = :folder ORDER BY date DESC") + fun loadAll(folder: Int, email: String): 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 87b3e0b32..d9326ff6c 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,17 +1,23 @@ package io.github.wulkanowy.data.db.dao -import androidx.room.* -import androidx.room.OnConflictStrategy.ABORT +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Transaction +import androidx.room.Update +import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student +import io.github.wulkanowy.data.db.entities.StudentName import io.github.wulkanowy.data.db.entities.StudentNickAndAvatar -import io.github.wulkanowy.data.db.entities.StudentWithSemesters import javax.inject.Singleton @Singleton @Dao abstract class StudentDao { - @Insert(onConflict = ABORT) + @Insert(onConflict = OnConflictStrategy.ABORT) abstract suspend fun insertAll(student: List): List @Delete @@ -20,6 +26,9 @@ abstract class StudentDao { @Update(entity = Student::class) abstract suspend fun update(studentNickAndAvatar: StudentNickAndAvatar) + @Update(entity = Student::class) + abstract suspend fun update(studentName: StudentName) + @Query("SELECT * FROM Students WHERE is_current = 1") abstract suspend fun loadCurrent(): Student? @@ -30,12 +39,12 @@ abstract class StudentDao { abstract suspend fun loadAll(): List @Transaction - @Query("SELECT * FROM Students") - abstract suspend fun loadStudentsWithSemesters(): List + @Query("SELECT * FROM Students JOIN Semesters ON Students.student_id = Semesters.student_id AND Students.class_id = Semesters.class_id") + abstract suspend fun loadStudentsWithSemesters(): Map> @Transaction - @Query("SELECT * FROM Students WHERE id = :id") - abstract suspend fun loadStudentWithSemestersById(id: Long): StudentWithSemesters? + @Query("SELECT * FROM Students JOIN Semesters ON Students.student_id = Semesters.student_id AND Students.class_id = Semesters.class_id WHERE Students.id = :id") + abstract suspend fun loadStudentWithSemestersById(id: Long): Map> @Query("UPDATE Students SET is_current = 1 WHERE id = :id") abstract suspend fun updateCurrent(id: Long) @@ -43,6 +52,9 @@ abstract class StudentDao { @Query("UPDATE Students SET is_current = 0") abstract suspend fun resetCurrent() + @Query("DELETE FROM Students WHERE email = :email AND user_name = :userName") + abstract suspend fun deleteByEmailAndUserName(email: String, userName: String) + @Transaction open suspend fun switchCurrent(id: Long) { resetCurrent() diff --git a/app/src/main/java/io/github/wulkanowy/data/db/dao/TimetableDao.kt b/app/src/main/java/io/github/wulkanowy/data/db/dao/TimetableDao.kt index 5e6eec668..b4b7379f2 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/dao/TimetableDao.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/dao/TimetableDao.kt @@ -13,4 +13,7 @@ interface TimetableDao : BaseDao { @Query("SELECT * FROM Timetable WHERE diary_id = :diaryId AND student_id = :studentId AND date >= :from AND date <= :end") fun loadAll(diaryId: Int, studentId: Int, from: LocalDate, end: LocalDate): Flow> + + @Query("SELECT * FROM Timetable WHERE diary_id = :diaryId AND student_id = :studentId AND date >= :from AND date <= :end") + fun load(diaryId: Int, studentId: Int, from: LocalDate, end: LocalDate): List } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/AdminMessage.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/AdminMessage.kt index 97fec69b7..0c8f1a5d1 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/entities/AdminMessage.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/AdminMessage.kt @@ -3,6 +3,7 @@ package io.github.wulkanowy.data.db.entities import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey +import io.github.wulkanowy.data.enums.MessageType import kotlinx.serialization.Serializable @Serializable @@ -33,8 +34,12 @@ data class AdminMessage( val priority: String, - val type: String, + @ColumnInfo(name = "types", defaultValue = "[]") + val types: List = emptyList(), - @ColumnInfo(name = "is_dismissible") - val isDismissible: Boolean = false + @ColumnInfo(name = "is_ok_visible", defaultValue = "0") + val isOkVisible: Boolean = false, + + @ColumnInfo(name = "is_x_visible", defaultValue = "0") + val isXVisible: Boolean = false ) diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/Exam.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/Exam.kt index 50299e607..2292c3e62 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/entities/Exam.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/Exam.kt @@ -22,6 +22,7 @@ data class Exam( val subject: String, + @Deprecated("not available anymore") val group: String, val type: String, diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/GradeDescriptive.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/GradeDescriptive.kt new file mode 100644 index 000000000..9aec9599a --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/GradeDescriptive.kt @@ -0,0 +1,27 @@ +package io.github.wulkanowy.data.db.entities + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import java.io.Serializable + +@Entity(tableName = "GradesDescriptive") +data class GradeDescriptive( + + @ColumnInfo(name = "semester_id") + val semesterId: Int, + + @ColumnInfo(name = "student_id") + val studentId: Int, + + val subject: String, + + val description: String, +) : Serializable { + + @PrimaryKey(autoGenerate = true) + var id: Long = 0 + + @ColumnInfo(name = "is_notified") + var isNotified: Boolean = true +} diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/Mailbox.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/Mailbox.kt index 7c08e481d..e65e213dd 100644 --- 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 @@ -1,20 +1,27 @@ package io.github.wulkanowy.data.db.entities +import android.os.Parcelable import androidx.room.Entity import androidx.room.PrimaryKey +import kotlinx.parcelize.Parcelize +@Parcelize @Entity(tableName = "Mailboxes") data class Mailbox( @PrimaryKey val globalKey: String, + + val email: String, + val symbol: String, + val schoolId: String, + val fullName: String, val userName: String, - val userLoginId: Int, val studentName: String, val schoolNameShort: String, val type: MailboxType, -) +) : java.io.Serializable, Parcelable enum class MailboxType { STUDENT, 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 77874e03d..d1356b33d 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 @@ -9,6 +9,9 @@ import java.time.Instant @Entity(tableName = "Messages") data class Message( + @ColumnInfo(name = "email") + val email: String, + @ColumnInfo(name = "message_global_key") val messageGlobalKey: String, @@ -29,6 +32,12 @@ data class Message( var unread: Boolean, + @ColumnInfo(name = "read_by") + val readBy: Int?, + + @ColumnInfo(name = "unread_by") + val unreadBy: Int?, + @ColumnInfo(name = "has_attachments") val hasAttachments: Boolean ) : Serializable { 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 93f042999..6f0c84ad7 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 @@ -2,16 +2,14 @@ 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 = "MessageAttachments") +@Entity( + tableName = "MessageAttachments", + primaryKeys = ["message_global_key", "url", "filename"], +) data class MessageAttachment( - @PrimaryKey - @ColumnInfo(name = "real_id") - val realId: Int, - @ColumnInfo(name = "message_global_key") val messageGlobalKey: String, 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 76da9643d..e1116733c 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 @@ -19,6 +19,9 @@ data class Student( @ColumnInfo(name = "scrapper_base_url") val scrapperBaseUrl: String, + @ColumnInfo(name = "scrapper_domain_suffix", defaultValue = "") + val scrapperDomainSuffix: String, + @ColumnInfo(name = "mobile_base_url") val mobileBaseUrl: String, diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/StudentName.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/StudentName.kt new file mode 100644 index 000000000..46f754b5e --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/StudentName.kt @@ -0,0 +1,18 @@ +package io.github.wulkanowy.data.db.entities + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import java.io.Serializable + +@Entity +data class StudentName( + + @ColumnInfo(name = "student_name") + val studentName: String + +) : Serializable { + + @PrimaryKey + var id: Long = 0 +} diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/StudentWithSemesters.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/StudentWithSemesters.kt index 9362a954e..f9869d4e2 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/entities/StudentWithSemesters.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/StudentWithSemesters.kt @@ -1,13 +1,8 @@ package io.github.wulkanowy.data.db.entities -import androidx.room.Embedded -import androidx.room.Relation import java.io.Serializable data class StudentWithSemesters( - @Embedded val student: Student, - - @Relation(parentColumn = "student_id", entityColumn = "student_id") val semesters: List ) : Serializable diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration10.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration10.kt index c26a02d1f..0e7e14097 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration10.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration10.kt @@ -5,7 +5,7 @@ import androidx.sqlite.db.SupportSQLiteDatabase class Migration10 : Migration(9, 10) { - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("ALTER TABLE Grades_Summary RENAME TO GradesSummary") + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("ALTER TABLE Grades_Summary RENAME TO GradesSummary") } } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration11.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration11.kt index 6d129bca0..342e2e2e3 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration11.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration11.kt @@ -5,8 +5,9 @@ import androidx.sqlite.db.SupportSQLiteDatabase class Migration11 : Migration(10, 11) { - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL(""" + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL( + """ CREATE TABLE IF NOT EXISTS Grades_temp ( id INTEGER PRIMARY KEY NOT NULL, is_read INTEGER NOT NULL, @@ -26,9 +27,10 @@ class Migration11 : Migration(10, 11) { date INTEGER NOT NULL, teacher TEXT NOT NULL ) - """) - database.execSQL("INSERT INTO Grades_temp SELECT * FROM Grades") - database.execSQL("DROP TABLE Grades") - database.execSQL("ALTER TABLE Grades_temp RENAME TO Grades") + """ + ) + db.execSQL("INSERT INTO Grades_temp SELECT * FROM Grades") + db.execSQL("DROP TABLE Grades") + db.execSQL("ALTER TABLE Grades_temp RENAME TO Grades") } } 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 c827b82ba..6cc726953 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 @@ -5,16 +5,17 @@ import androidx.sqlite.db.SupportSQLiteDatabase class Migration12 : Migration(11, 12) { - override fun migrate(database: SupportSQLiteDatabase) { - createTempStudentsTable(database) - replaceStudentTable(database) - updateStudentsWithClassId(database, getStudentsIds(database)) - removeStudentsWithNoClassId(database) - ensureThereIsOnlyOneCurrentStudent(database) + override fun migrate(db: SupportSQLiteDatabase) { + createTempStudentsTable(db) + replaceStudentTable(db) + updateStudentsWithClassId(db, getStudentsIds(db)) + removeStudentsWithNoClassId(db) + ensureThereIsOnlyOneCurrentStudent(db) } - private fun createTempStudentsTable(database: SupportSQLiteDatabase) { - database.execSQL(""" + private fun createTempStudentsTable(db: SupportSQLiteDatabase) { + db.execSQL( + """ CREATE TABLE IF NOT EXISTS Students_tmp ( id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, endpoint TEXT NOT NULL, @@ -30,15 +31,16 @@ class Migration12 : Migration(11, 12) { registration_date INTEGER NOT NULL, class_id INTEGER NOT NULL ) - """) - database.execSQL("CREATE UNIQUE INDEX index_Students_email_symbol_student_id_school_id_class_id ON Students_tmp (email, symbol, student_id, school_id, class_id)") + """ + ) + db.execSQL("CREATE UNIQUE INDEX index_Students_email_symbol_student_id_school_id_class_id ON Students_tmp (email, symbol, student_id, school_id, class_id)") } - private fun replaceStudentTable(database: SupportSQLiteDatabase) { - database.execSQL("ALTER TABLE Students ADD COLUMN class_id INTEGER DEFAULT 0 NOT NULL") - database.execSQL("INSERT INTO Students_tmp SELECT * FROM Students") - database.execSQL("DROP TABLE Students") - database.execSQL("ALTER TABLE Students_tmp RENAME TO Students") + private fun replaceStudentTable(db: SupportSQLiteDatabase) { + db.execSQL("ALTER TABLE Students ADD COLUMN class_id INTEGER DEFAULT 0 NOT NULL") + db.execSQL("INSERT INTO Students_tmp SELECT * FROM Students") + db.execSQL("DROP TABLE Students") + db.execSQL("ALTER TABLE Students_tmp RENAME TO Students") } private fun getStudentsIds(database: SupportSQLiteDatabase): List { @@ -54,18 +56,18 @@ class Migration12 : Migration(11, 12) { return students } - private fun updateStudentsWithClassId(database: SupportSQLiteDatabase, students: List) { + private fun updateStudentsWithClassId(db: SupportSQLiteDatabase, students: List) { students.forEach { - database.execSQL("UPDATE Students SET class_id = IFNULL((SELECT class_id FROM Semesters WHERE student_id = $it), 0) WHERE student_id = $it") + db.execSQL("UPDATE Students SET class_id = IFNULL((SELECT class_id FROM Semesters WHERE student_id = $it), 0) WHERE student_id = $it") } } - private fun removeStudentsWithNoClassId(database: SupportSQLiteDatabase) { - database.execSQL("DELETE FROM Students WHERE class_id = 0") + private fun removeStudentsWithNoClassId(db: SupportSQLiteDatabase) { + db.execSQL("DELETE FROM Students WHERE class_id = 0") } - private fun ensureThereIsOnlyOneCurrentStudent(database: SupportSQLiteDatabase) { - database.execSQL("UPDATE Students SET is_current = 0") - database.execSQL("UPDATE Students SET is_current = 1 WHERE id = (SELECT MAX(id) FROM Students)") + private fun ensureThereIsOnlyOneCurrentStudent(db: SupportSQLiteDatabase) { + db.execSQL("UPDATE Students SET is_current = 0") + db.execSQL("UPDATE Students SET is_current = 1 WHERE id = (SELECT MAX(id) FROM 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 36de1e837..c5030232b 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 @@ -5,27 +5,30 @@ import androidx.sqlite.db.SupportSQLiteDatabase class Migration13 : Migration(12, 13) { - override fun migrate(database: SupportSQLiteDatabase) { - addClassNameToStudents(database, getStudentsIds(database)) - updateSemestersTable(database) - markAtLeastAndOnlyOneSemesterAtCurrent(database, getStudentsAndClassIds(database)) - clearMessagesTable(database) + override fun migrate(db: SupportSQLiteDatabase) { + addClassNameToStudents(db, getStudentsIds(db)) + updateSemestersTable(db) + markAtLeastAndOnlyOneSemesterAtCurrent(db, getStudentsAndClassIds(db)) + clearMessagesTable(db) } - private fun addClassNameToStudents(database: SupportSQLiteDatabase, students: List>) { - database.execSQL("ALTER TABLE Students ADD COLUMN class_name TEXT DEFAULT \"\" NOT NULL") + private fun addClassNameToStudents( + db: SupportSQLiteDatabase, + students: List> + ) { + db.execSQL("ALTER TABLE Students ADD COLUMN class_name TEXT DEFAULT \"\" NOT NULL") students.forEach { (id, name) -> val schoolName = name.substringAfter(" - ") val className = name.substringBefore(" - ", "").replace("Klasa ", "") - database.execSQL("UPDATE Students SET class_name = '$className' WHERE id = '$id'") - database.execSQL("UPDATE Students SET school_name = '$schoolName' WHERE id = '$id'") + db.execSQL("UPDATE Students SET class_name = '$className' WHERE id = '$id'") + db.execSQL("UPDATE Students SET school_name = '$schoolName' WHERE id = '$id'") } } - private fun getStudentsIds(database: SupportSQLiteDatabase): MutableList> { + private fun getStudentsIds(db: SupportSQLiteDatabase): MutableList> { val students = mutableListOf>() - database.query("SELECT id, school_name FROM Students").use { + db.query("SELECT id, school_name FROM Students").use { if (it.moveToFirst()) { do { students.add(it.getInt(0) to it.getString(1)) @@ -36,15 +39,15 @@ class Migration13 : Migration(12, 13) { return students } - private fun updateSemestersTable(database: SupportSQLiteDatabase) { - database.execSQL("ALTER TABLE Semesters ADD COLUMN school_year INTEGER DEFAULT 1970 NOT NULL") - database.execSQL("ALTER TABLE Semesters ADD COLUMN start INTEGER DEFAULT 0 NOT NULL") - database.execSQL("ALTER TABLE Semesters ADD COLUMN `end` INTEGER DEFAULT 0 NOT NULL") + private fun updateSemestersTable(db: SupportSQLiteDatabase) { + db.execSQL("ALTER TABLE Semesters ADD COLUMN school_year INTEGER DEFAULT 1970 NOT NULL") + db.execSQL("ALTER TABLE Semesters ADD COLUMN start INTEGER DEFAULT 0 NOT NULL") + db.execSQL("ALTER TABLE Semesters ADD COLUMN `end` INTEGER DEFAULT 0 NOT NULL") } - private fun getStudentsAndClassIds(database: SupportSQLiteDatabase): List> { + private fun getStudentsAndClassIds(db: SupportSQLiteDatabase): List> { val students = mutableListOf>() - database.query("SELECT student_id, class_id FROM Students").use { + db.query("SELECT student_id, class_id FROM Students").use { if (it.moveToFirst()) { do { students.add(it.getInt(0) to it.getInt(1)) @@ -55,14 +58,17 @@ class Migration13 : Migration(12, 13) { return students } - private fun markAtLeastAndOnlyOneSemesterAtCurrent(database: SupportSQLiteDatabase, students: List>) { + private fun markAtLeastAndOnlyOneSemesterAtCurrent( + db: SupportSQLiteDatabase, + students: List> + ) { students.forEach { (studentId, classId) -> - database.execSQL("UPDATE Semesters SET is_current = 0 WHERE student_id = '$studentId' AND class_id = '$classId'") - database.execSQL("UPDATE Semesters SET is_current = 1 WHERE id = (SELECT id FROM Semesters WHERE student_id = '$studentId' AND class_id = '$classId' ORDER BY semester_id DESC)") + db.execSQL("UPDATE Semesters SET is_current = 0 WHERE student_id = '$studentId' AND class_id = '$classId'") + db.execSQL("UPDATE Semesters SET is_current = 1 WHERE id = (SELECT id FROM Semesters WHERE student_id = '$studentId' AND class_id = '$classId' ORDER BY semester_id DESC)") } } - private fun clearMessagesTable(database: SupportSQLiteDatabase) { - database.execSQL("DELETE FROM Messages") + private fun clearMessagesTable(db: SupportSQLiteDatabase) { + db.execSQL("DELETE FROM Messages") } } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration14.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration14.kt index 4dac0d306..793b4a9d2 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration14.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration14.kt @@ -5,9 +5,10 @@ import androidx.sqlite.db.SupportSQLiteDatabase class Migration14 : Migration(13, 14) { - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("DROP TABLE IF EXISTS GradesSummary") - database.execSQL(""" + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("DROP TABLE IF EXISTS GradesSummary") + db.execSQL( + """ CREATE TABLE IF NOT EXISTS GradesSummary ( id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, semester_id INTEGER NOT NULL, diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration15.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration15.kt index 5be49a95b..5ff44e9ca 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration15.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration15.kt @@ -5,8 +5,9 @@ import androidx.sqlite.db.SupportSQLiteDatabase class Migration15 : Migration(14, 15) { - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL(""" + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL( + """ CREATE TABLE IF NOT EXISTS MobileDevices ( id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, student_id INTEGER NOT NULL, @@ -14,6 +15,7 @@ class Migration15 : Migration(14, 15) { name TEXT NOT NULL, date INTEGER NOT NULL ) - """) + """ + ) } } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration16.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration16.kt index 7f40c0f8d..8a8f5b8f2 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration16.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration16.kt @@ -5,8 +5,9 @@ import androidx.sqlite.db.SupportSQLiteDatabase class Migration16 : Migration(15, 16) { - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL(""" + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL( + """ CREATE TABLE IF NOT EXISTS Teachers ( id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, student_id INTEGER NOT NULL, @@ -15,6 +16,7 @@ class Migration16 : Migration(15, 16) { name TEXT NOT NULL, short_name TEXT NOT NULL ) - """) + """ + ) } } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration17.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration17.kt index e2a2574db..cf3318ad4 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration17.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration17.kt @@ -5,13 +5,14 @@ import androidx.sqlite.db.SupportSQLiteDatabase class Migration17 : Migration(16, 17) { - override fun migrate(database: SupportSQLiteDatabase) { - createGradesPointsStatisticsTable(database) - truncateSemestersTable(database) + override fun migrate(db: SupportSQLiteDatabase) { + createGradesPointsStatisticsTable(db) + truncateSemestersTable(db) } - private fun createGradesPointsStatisticsTable(database: SupportSQLiteDatabase) { - database.execSQL(""" + private fun createGradesPointsStatisticsTable(db: SupportSQLiteDatabase) { + db.execSQL( + """ CREATE TABLE IF NOT EXISTS GradesPointsStatistics( id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, student_id INTEGER NOT NULL, @@ -20,10 +21,11 @@ class Migration17 : Migration(16, 17) { others REAL NOT NULL, student REAL NOT NULL ) - """) + """ + ) } - private fun truncateSemestersTable(database: SupportSQLiteDatabase) { - database.execSQL("DELETE FROM Semesters") + private fun truncateSemestersTable(db: SupportSQLiteDatabase) { + db.execSQL("DELETE FROM Semesters") } } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration18.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration18.kt index 6c5e56c6a..713f8e724 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration18.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration18.kt @@ -5,8 +5,9 @@ import androidx.sqlite.db.SupportSQLiteDatabase class Migration18 : Migration(17, 18) { - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL(""" + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL( + """ CREATE TABLE IF NOT EXISTS School ( id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, student_id INTEGER NOT NULL, diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration19.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration19.kt index d38f1245a..021cdbb37 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration19.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration19.kt @@ -6,16 +6,17 @@ import io.github.wulkanowy.data.db.SharedPrefProvider class Migration19(private val sharedPrefProvider: SharedPrefProvider) : Migration(18, 19) { - override fun migrate(database: SupportSQLiteDatabase) { - migrateMessages(database) - migrateGrades(database) - migrateStudents(database) + override fun migrate(db: SupportSQLiteDatabase) { + migrateMessages(db) + migrateGrades(db) + migrateStudents(db) migrateSharedPreferences() } - private fun migrateMessages(database: SupportSQLiteDatabase) { - database.execSQL("DROP TABLE Messages") - database.execSQL(""" + private fun migrateMessages(db: SupportSQLiteDatabase) { + db.execSQL("DROP TABLE Messages") + db.execSQL( + """ CREATE TABLE IF NOT EXISTS Messages ( id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, is_notified INTEGER NOT NULL, @@ -34,12 +35,14 @@ class Migration19(private val sharedPrefProvider: SharedPrefProvider) : Migratio read_by INTEGER NOT NULL, removed INTEGER NOT NULL ) - """) + """ + ) } - private fun migrateGrades(database: SupportSQLiteDatabase) { - database.execSQL("DROP TABLE Grades") - database.execSQL(""" + private fun migrateGrades(db: SupportSQLiteDatabase) { + db.execSQL("DROP TABLE Grades") + db.execSQL( + """ CREATE TABLE IF NOT EXISTS Grades ( id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, is_read INTEGER NOT NULL, @@ -59,11 +62,13 @@ class Migration19(private val sharedPrefProvider: SharedPrefProvider) : Migratio date INTEGER NOT NULL, teacher TEXT NOT NULL ) - """) + """ + ) } - private fun migrateStudents(database: SupportSQLiteDatabase) { - database.execSQL(""" + private fun migrateStudents(db: SupportSQLiteDatabase) { + db.execSQL( + """ CREATE TABLE IF NOT EXISTS Students_tmp ( id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, scrapper_base_url TEXT NOT NULL, @@ -86,26 +91,29 @@ class Migration19(private val sharedPrefProvider: SharedPrefProvider) : Migratio is_current INTEGER NOT NULL, registration_date INTEGER NOT NULL ) - """) + """ + ) - database.execSQL("ALTER TABLE Students ADD COLUMN scrapperBaseUrl TEXT NOT NULL DEFAULT \"\";") - database.execSQL("ALTER TABLE Students ADD COLUMN apiBaseUrl TEXT NOT NULL DEFAULT \"\";") - database.execSQL("ALTER TABLE Students ADD COLUMN is_parent INT NOT NULL DEFAULT 0;") - database.execSQL("ALTER TABLE Students ADD COLUMN loginMode TEXT NOT NULL DEFAULT \"\";") - database.execSQL("ALTER TABLE Students ADD COLUMN certificateKey TEXT NOT NULL DEFAULT \"\";") - database.execSQL("ALTER TABLE Students ADD COLUMN privateKey TEXT NOT NULL DEFAULT \"\";") - database.execSQL("ALTER TABLE Students ADD COLUMN user_login_id INTEGER NOT NULL DEFAULT 0;") + db.execSQL("ALTER TABLE Students ADD COLUMN scrapperBaseUrl TEXT NOT NULL DEFAULT \"\";") + db.execSQL("ALTER TABLE Students ADD COLUMN apiBaseUrl TEXT NOT NULL DEFAULT \"\";") + db.execSQL("ALTER TABLE Students ADD COLUMN is_parent INT NOT NULL DEFAULT 0;") + db.execSQL("ALTER TABLE Students ADD COLUMN loginMode TEXT NOT NULL DEFAULT \"\";") + db.execSQL("ALTER TABLE Students ADD COLUMN certificateKey TEXT NOT NULL DEFAULT \"\";") + db.execSQL("ALTER TABLE Students ADD COLUMN privateKey TEXT NOT NULL DEFAULT \"\";") + db.execSQL("ALTER TABLE Students ADD COLUMN user_login_id INTEGER NOT NULL DEFAULT 0;") - database.execSQL(""" + db.execSQL( + """ INSERT INTO Students_tmp( id, scrapper_base_url, mobile_base_url, is_parent, login_type, login_mode, certificate_key, private_key, email, password, symbol, student_id, user_login_id, student_name, school_id, school_name, school_id, school_name, class_name, class_id, is_current, registration_date) SELECT id, endpoint, apiBaseUrl, is_parent, loginType, "SCRAPPER", certificateKey, privateKey, email, password, symbol, student_id, user_login_id, student_name, school_id, school_name, school_id, school_name, class_name, class_id, is_current, registration_date FROM Students - """) - database.execSQL("DROP TABLE Students") - database.execSQL("ALTER TABLE Students_tmp RENAME TO Students") - database.execSQL("CREATE UNIQUE INDEX index_Students_email_symbol_student_id_school_id_class_id ON Students (email, symbol, student_id, school_id, class_id)") + """ + ) + db.execSQL("DROP TABLE Students") + db.execSQL("ALTER TABLE Students_tmp RENAME TO Students") + db.execSQL("CREATE UNIQUE INDEX index_Students_email_symbol_student_id_school_id_class_id ON Students (email, symbol, student_id, school_id, class_id)") } private fun migrateSharedPreferences() { diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration2.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration2.kt index c5a30991a..be8675092 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration2.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration2.kt @@ -5,14 +5,16 @@ import androidx.sqlite.db.SupportSQLiteDatabase class Migration2 : Migration(1, 2) { - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL(""" + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL( + """ CREATE TABLE IF NOT EXISTS LuckyNumbers ( id INTEGER PRIMARY KEY NOT NULL, is_notified INTEGER NOT NULL, student_id INTEGER NOT NULL, date INTEGER NOT NULL, lucky_number INTEGER NOT NULL) - """) + """ + ) } } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration20.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration20.kt index 2fcfc183d..7ad43230b 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration20.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration20.kt @@ -5,14 +5,15 @@ import androidx.sqlite.db.SupportSQLiteDatabase class Migration20 : Migration(19, 20) { - override fun migrate(database: SupportSQLiteDatabase) { - migrateTimetable(database) - truncateSubjects(database) + override fun migrate(db: SupportSQLiteDatabase) { + migrateTimetable(db) + truncateSubjects(db) } - private fun migrateTimetable(database: SupportSQLiteDatabase) { - database.execSQL("DROP TABLE Timetable") - database.execSQL(""" + private fun migrateTimetable(db: SupportSQLiteDatabase) { + db.execSQL("DROP TABLE Timetable") + db.execSQL( + """ CREATE TABLE IF NOT EXISTS `Timetable` ( `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `student_id` INTEGER NOT NULL, @@ -33,10 +34,11 @@ class Migration20 : Migration(19, 20) { `changes` INTEGER NOT NULL, `canceled` INTEGER NOT NULL ) - """) + """ + ) } - private fun truncateSubjects(database: SupportSQLiteDatabase) { - database.execSQL("DELETE FROM Subjects") + private fun truncateSubjects(db: SupportSQLiteDatabase) { + db.execSQL("DELETE FROM Subjects") } } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration21.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration21.kt index bc0ff900c..60e044cdc 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration21.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration21.kt @@ -5,11 +5,11 @@ import androidx.sqlite.db.SupportSQLiteDatabase class Migration21 : Migration(20, 21) { - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("ALTER TABLE Attendance ADD COLUMN excusable INTEGER NOT NULL DEFAULT 0") - database.execSQL("ALTER TABLE Attendance ADD COLUMN time_id INTEGER NOT NULL DEFAULT 0") - database.execSQL("ALTER TABLE Attendance ADD COLUMN excuse_status TEXT DEFAULT NULL") + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("ALTER TABLE Attendance ADD COLUMN excusable INTEGER NOT NULL DEFAULT 0") + db.execSQL("ALTER TABLE Attendance ADD COLUMN time_id INTEGER NOT NULL DEFAULT 0") + db.execSQL("ALTER TABLE Attendance ADD COLUMN excuse_status TEXT DEFAULT NULL") - database.execSQL("DELETE FROM Semesters") + db.execSQL("DELETE FROM Semesters") } } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration22.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration22.kt index cf50a6c3e..ef525a49d 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration22.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration22.kt @@ -5,7 +5,7 @@ import androidx.sqlite.db.SupportSQLiteDatabase class Migration22 : Migration(21, 22) { - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("ALTER TABLE Students ADD COLUMN school_short TEXT NOT NULL DEFAULT ''") + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("ALTER TABLE Students ADD COLUMN school_short TEXT NOT NULL DEFAULT ''") } } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration23.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration23.kt index 22de94c3f..3650307a6 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration23.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration23.kt @@ -5,10 +5,10 @@ import androidx.sqlite.db.SupportSQLiteDatabase class Migration23 : Migration(22, 23) { - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("ALTER TABLE Notes ADD COLUMN teacher_symbol TEXT NOT NULL DEFAULT ''") - database.execSQL("ALTER TABLE Notes ADD COLUMN category_type INTEGER NOT NULL DEFAULT 0") - database.execSQL("ALTER TABLE Notes ADD COLUMN is_points_show INTEGER NOT NULL DEFAULT 0") - database.execSQL("ALTER TABLE Notes ADD COLUMN points INTEGER NOT NULL DEFAULT 0") + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("ALTER TABLE Notes ADD COLUMN teacher_symbol TEXT NOT NULL DEFAULT ''") + db.execSQL("ALTER TABLE Notes ADD COLUMN category_type INTEGER NOT NULL DEFAULT 0") + db.execSQL("ALTER TABLE Notes ADD COLUMN is_points_show INTEGER NOT NULL DEFAULT 0") + db.execSQL("ALTER TABLE Notes ADD COLUMN points INTEGER NOT NULL DEFAULT 0") } } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration24.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration24.kt index 604ed4875..a3cd98197 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration24.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration24.kt @@ -5,9 +5,10 @@ import androidx.sqlite.db.SupportSQLiteDatabase class Migration24 : Migration(23, 24) { - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("ALTER TABLE Messages ADD COLUMN has_attachments INTEGER NOT NULL DEFAULT 0") - database.execSQL(""" + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("ALTER TABLE Messages ADD COLUMN has_attachments INTEGER NOT NULL DEFAULT 0") + db.execSQL( + """ CREATE TABLE IF NOT EXISTS MessageAttachments ( real_id INTEGER NOT NULL, message_id INTEGER NOT NULL, @@ -16,6 +17,7 @@ class Migration24 : Migration(23, 24) { filename TEXT NOT NULL, PRIMARY KEY(real_id) ) - """) + """ + ) } } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration25.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration25.kt index 4749bac73..cb395d7e0 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration25.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration25.kt @@ -5,8 +5,8 @@ import androidx.sqlite.db.SupportSQLiteDatabase class Migration25 : Migration(24, 25) { - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("ALTER TABLE Homework ADD COLUMN is_done INTEGER NOT NULL DEFAULT 0") - database.execSQL("ALTER TABLE Homework ADD COLUMN attachments TEXT NOT NULL DEFAULT \"[]\"") + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("ALTER TABLE Homework ADD COLUMN is_done INTEGER NOT NULL DEFAULT 0") + db.execSQL("ALTER TABLE Homework ADD COLUMN attachments TEXT NOT NULL DEFAULT \"[]\"") } } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration26.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration26.kt index 7130d86d8..94746b456 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration26.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration26.kt @@ -5,10 +5,10 @@ import androidx.sqlite.db.SupportSQLiteDatabase class Migration26 : Migration(25, 26) { - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("ALTER TABLE GradesSummary ADD COLUMN is_predicted_grade_notified INTEGER NOT NULL DEFAULT 1") - database.execSQL("ALTER TABLE GradesSummary ADD COLUMN is_final_grade_notified INTEGER NOT NULL DEFAULT 1") - database.execSQL("ALTER TABLE GradesSummary ADD COLUMN predicted_grade_last_change INTEGER NOT NULL DEFAULT 0") - database.execSQL("ALTER TABLE GradesSummary ADD COLUMN final_grade_last_change INTEGER NOT NULL DEFAULT 0") + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("ALTER TABLE GradesSummary ADD COLUMN is_predicted_grade_notified INTEGER NOT NULL DEFAULT 1") + db.execSQL("ALTER TABLE GradesSummary ADD COLUMN is_final_grade_notified INTEGER NOT NULL DEFAULT 1") + db.execSQL("ALTER TABLE GradesSummary ADD COLUMN predicted_grade_last_change INTEGER NOT NULL DEFAULT 0") + db.execSQL("ALTER TABLE GradesSummary ADD COLUMN final_grade_last_change INTEGER NOT NULL DEFAULT 0") } } 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 5c60beead..a7ba763df 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 @@ -5,24 +5,25 @@ import androidx.sqlite.db.SupportSQLiteDatabase class Migration27 : Migration(26, 27) { - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("ALTER TABLE Students ADD COLUMN user_name TEXT NOT NULL DEFAULT \"\"") + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("ALTER TABLE Students ADD COLUMN user_name TEXT NOT NULL DEFAULT \"\"") - val students = getStudentsIdsAndNames(database) - val units = getReportingUnits(database) + val students = getStudentsIdsAndNames(db) + val units = getReportingUnits(db) students.forEach { (id, userLoginId, studentName) -> - val userNameFromUnits = units.singleOrNull { (senderId, _) -> senderId == userLoginId }?.second + val userNameFromUnits = + units.singleOrNull { (senderId, _) -> senderId == userLoginId }?.second val normalizedStudentName = studentName.split(" ").asReversed().joinToString(" ") val userName = userNameFromUnits ?: normalizedStudentName - database.execSQL("UPDATE Students SET user_name = '$userName' WHERE id = '$id'") + db.execSQL("UPDATE Students SET user_name = '$userName' WHERE id = '$id'") } } - private fun getStudentsIdsAndNames(database: SupportSQLiteDatabase): MutableList> { + private fun getStudentsIdsAndNames(db: SupportSQLiteDatabase): MutableList> { val students = mutableListOf>() - database.query("SELECT id, user_login_id, student_name FROM Students").use { + db.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))) @@ -33,9 +34,9 @@ class Migration27 : Migration(26, 27) { return students } - private fun getReportingUnits(database: SupportSQLiteDatabase): MutableList> { + private fun getReportingUnits(db: SupportSQLiteDatabase): MutableList> { val units = mutableListOf>() - database.query("SELECT sender_id, sender_name FROM ReportingUnits").use { + db.query("SELECT sender_id, sender_name FROM ReportingUnits").use { if (it.moveToFirst()) { do { units.add(it.getInt(0) to it.getString(1)) diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration28.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration28.kt index 51e7628b5..e8a5a4a86 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration28.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration28.kt @@ -5,8 +5,9 @@ import androidx.sqlite.db.SupportSQLiteDatabase class Migration28 : Migration(27, 28) { - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL(""" + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL( + """ CREATE TABLE IF NOT EXISTS Conferences ( id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, student_id INTEGER NOT NULL, diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration29.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration29.kt index 327552d75..dac303d27 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration29.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration29.kt @@ -5,9 +5,10 @@ import androidx.sqlite.db.SupportSQLiteDatabase class Migration29 : Migration(28, 29) { - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("DROP TABLE IF EXISTS GradesStatistics") - database.execSQL(""" + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("DROP TABLE IF EXISTS GradesStatistics") + db.execSQL( + """ CREATE TABLE IF NOT EXISTS GradeSemesterStatistics ( id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, student_id INTEGER NOT NULL, @@ -16,8 +17,10 @@ class Migration29 : Migration(28, 29) { amounts TEXT NOT NULL, student_grade INTEGER NOT NULL ) - """) - database.execSQL(""" + """ + ) + db.execSQL( + """ CREATE TABLE IF NOT EXISTS GradePartialStatistics ( id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, student_id INTEGER NOT NULL, diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration3.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration3.kt index d9699c0f4..44d421648 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration3.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration3.kt @@ -5,8 +5,9 @@ import androidx.sqlite.db.SupportSQLiteDatabase class Migration3 : Migration(2, 3) { - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL(""" + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL( + """ CREATE TABLE IF NOT EXISTS CompletedLesson ( id INTEGER PRIMARY KEY NOT NULL, student_id INTEGER NOT NULL, diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration30.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration30.kt index b33914fec..3fea8ec0e 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration30.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration30.kt @@ -5,8 +5,9 @@ import androidx.sqlite.db.SupportSQLiteDatabase class Migration30 : Migration(29, 30) { - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL(""" + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL( + """ CREATE TABLE TimetableAdditional ( id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, student_id INTEGER NOT NULL, @@ -16,6 +17,7 @@ class Migration30 : Migration(29, 30) { date INTEGER NOT NULL, subject TEXT NOT NULL ) - """) + """ + ) } } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration31.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration31.kt index 064a3e5bc..28fb10562 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration31.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration31.kt @@ -5,8 +5,8 @@ import androidx.sqlite.db.SupportSQLiteDatabase class Migration31 : Migration(30, 31) { - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL( + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL( """CREATE TABLE IF NOT EXISTS StudentInfo ( id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, student_id INTEGER NOT NULL, diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration32.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration32.kt index 508485e08..347873936 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration32.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration32.kt @@ -5,8 +5,8 @@ import androidx.sqlite.db.SupportSQLiteDatabase class Migration32 : Migration(31, 32) { - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("ALTER TABLE Students ADD COLUMN nick TEXT NOT NULL DEFAULT \"\"") + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("ALTER TABLE Students ADD COLUMN nick TEXT NOT NULL DEFAULT \"\"") } } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration33.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration33.kt index 4a57880d4..9778d2790 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration33.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration33.kt @@ -5,10 +5,10 @@ import androidx.sqlite.db.SupportSQLiteDatabase class Migration33 : Migration(32, 33) { - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("DROP TABLE IF EXISTS StudentInfo") + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("DROP TABLE IF EXISTS StudentInfo") - database.execSQL( + db.execSQL( """CREATE TABLE IF NOT EXISTS StudentInfo ( id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, student_id INTEGER NOT NULL, diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration34.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration34.kt index 2c57eb00a..e9eec58cd 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration34.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration34.kt @@ -5,9 +5,9 @@ import androidx.sqlite.db.SupportSQLiteDatabase class Migration34 : Migration(33, 34) { - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("DELETE FROM ReportingUnits") - database.execSQL("DELETE FROM Recipients") + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("DELETE FROM ReportingUnits") + db.execSQL("DELETE FROM Recipients") } } 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 f63431d00..b238ce8b4 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 @@ -7,13 +7,13 @@ import io.github.wulkanowy.utils.AppInfo 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") + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("ALTER TABLE Students ADD COLUMN `avatar_color` INTEGER NOT NULL DEFAULT 0") - database.query("SELECT * FROM Students").use { + db.query("SELECT * FROM Students").use { while (it.moveToNext()) { val studentId = it.getLongOrNull(0) - database.execSQL( + db.execSQL( """ UPDATE Students SET avatar_color = ${appInfo.defaultColorsForAvatar.random()} diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration36.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration36.kt index 7ea106585..62ce346cd 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration36.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration36.kt @@ -5,8 +5,8 @@ import androidx.sqlite.db.SupportSQLiteDatabase class Migration36 : Migration(35, 36) { - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("ALTER TABLE Exams ADD COLUMN is_notified INTEGER NOT NULL DEFAULT 1") - database.execSQL("ALTER TABLE Homework ADD COLUMN is_notified INTEGER NOT NULL DEFAULT 1") + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("ALTER TABLE Exams ADD COLUMN is_notified INTEGER NOT NULL DEFAULT 1") + db.execSQL("ALTER TABLE Homework ADD COLUMN is_notified INTEGER NOT NULL DEFAULT 1") } } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration37.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration37.kt index a3fcd51a6..9ab35514f 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration37.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration37.kt @@ -5,8 +5,8 @@ import androidx.sqlite.db.SupportSQLiteDatabase class Migration37 : Migration(36, 37) { - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL( + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL( """ CREATE TABLE IF NOT EXISTS TimetableHeaders ( id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration38.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration38.kt index 1f90f5a44..bb9b32bfa 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration38.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration38.kt @@ -5,8 +5,9 @@ import androidx.sqlite.db.SupportSQLiteDatabase class Migration38 : Migration(37, 38) { - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL(""" + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL( + """ CREATE TABLE IF NOT EXISTS `SchoolAnnouncements` ( `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `student_id` INTEGER NOT NULL, @@ -14,6 +15,7 @@ class Migration38 : Migration(37, 38) { `subject` TEXT NOT NULL, `content` TEXT NOT NULL ) - """) + """ + ) } } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration39.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration39.kt index 6c0d36dd2..2e5315bf4 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration39.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration39.kt @@ -5,8 +5,8 @@ import androidx.sqlite.db.SupportSQLiteDatabase class Migration39 : Migration(38, 39) { - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("ALTER TABLE Conferences ADD COLUMN is_notified INTEGER NOT NULL DEFAULT 1") - database.execSQL("ALTER TABLE SchoolAnnouncements ADD COLUMN is_notified INTEGER NOT NULL DEFAULT 1") + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("ALTER TABLE Conferences ADD COLUMN is_notified INTEGER NOT NULL DEFAULT 1") + db.execSQL("ALTER TABLE SchoolAnnouncements ADD COLUMN is_notified INTEGER NOT NULL DEFAULT 1") } -} \ No newline at end of file +} diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration4.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration4.kt index 0ae89bdd6..b6089aa62 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration4.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration4.kt @@ -5,9 +5,10 @@ import androidx.sqlite.db.SupportSQLiteDatabase class Migration4 : Migration(3, 4) { - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("DROP TABLE IF EXISTS Messages") - database.execSQL(""" + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("DROP TABLE IF EXISTS Messages") + db.execSQL( + """ CREATE TABLE IF NOT EXISTS Messages ( id INTEGER PRIMARY KEY NOT NULL, is_notified INTEGER NOT NULL, diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration40.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration40.kt index 6d2795c7c..8e38b0c84 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration40.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration40.kt @@ -5,8 +5,8 @@ import androidx.sqlite.db.SupportSQLiteDatabase class Migration40 : Migration(39, 40) { - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL( + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL( """ CREATE TABLE IF NOT EXISTS `Notifications` ( `student_id` INTEGER NOT NULL, @@ -20,4 +20,4 @@ class Migration40 : Migration(39, 40) { """ ) } -} \ No newline at end of file +} diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration41.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration41.kt index ccaf85755..bfc28334b 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration41.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration41.kt @@ -7,9 +7,9 @@ import io.github.wulkanowy.data.enums.GradeExpandMode class Migration41(private val sharedPrefProvider: SharedPrefProvider) : Migration(40, 41) { - override fun migrate(database: SupportSQLiteDatabase) { + override fun migrate(db: SupportSQLiteDatabase) { migrateSharedPreferences() - database.execSQL("ALTER TABLE Homework ADD COLUMN is_added_by_user INTEGER NOT NULL DEFAULT 0") + db.execSQL("ALTER TABLE Homework ADD COLUMN is_added_by_user INTEGER NOT NULL DEFAULT 0") } private fun migrateSharedPreferences() { @@ -18,4 +18,4 @@ class Migration41(private val sharedPrefProvider: SharedPrefProvider) : Migratio } sharedPrefProvider.delete("pref_key_expand_grade") } -} \ No newline at end of file +} diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration42.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration42.kt index 3d66f301b..14356e279 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration42.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration42.kt @@ -5,8 +5,8 @@ import androidx.sqlite.db.SupportSQLiteDatabase class Migration42 : Migration(41, 42) { - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL( + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL( """CREATE TABLE IF NOT EXISTS `AdminMessages` ( `id` INTEGER NOT NULL, `title` TEXT NOT NULL, @@ -21,4 +21,4 @@ class Migration42 : Migration(41, 42) { PRIMARY KEY(`id`))""" ) } -} \ No newline at end of file +} diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration43.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration43.kt index 68c2834d6..ef8108166 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration43.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration43.kt @@ -5,8 +5,8 @@ import androidx.sqlite.db.SupportSQLiteDatabase class Migration43 : Migration(42, 43) { - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("ALTER TABLE Timetable ADD COLUMN is_notified INTEGER NOT NULL DEFAULT 1") - database.execSQL("ALTER TABLE Attendance ADD COLUMN is_notified INTEGER NOT NULL DEFAULT 1") + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("ALTER TABLE Timetable ADD COLUMN is_notified INTEGER NOT NULL DEFAULT 1") + db.execSQL("ALTER TABLE Attendance ADD COLUMN is_notified INTEGER NOT NULL DEFAULT 1") } } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration44.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration44.kt index 7bdcab5f4..0a4e5f962 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration44.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration44.kt @@ -5,7 +5,7 @@ import androidx.sqlite.db.SupportSQLiteDatabase class Migration44 : Migration(43, 44) { - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("ALTER TABLE AdminMessages ADD COLUMN is_dismissible INTEGER NOT NULL DEFAULT 0") + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("ALTER TABLE AdminMessages ADD COLUMN is_dismissible INTEGER NOT NULL DEFAULT 0") } } 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 index d3fa5cf93..0bacbaa0e 100644 --- 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 @@ -8,65 +8,65 @@ 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) + override fun migrate(db: SupportSQLiteDatabase) { + migrateConferences(db) + migrateMessages(db) + migrateMobileDevices(db) + migrateNotifications(db) + migrateTimetable(db) + migrateTimetableAdditional(db) } - private fun migrateConferences(database: SupportSQLiteDatabase) { - database.query("SELECT * FROM Conferences").use { + private fun migrateConferences(db: SupportSQLiteDatabase) { + db.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") + db.execSQL("UPDATE Conferences SET date = $timestampUtc WHERE id = $id") } } } - private fun migrateMessages(database: SupportSQLiteDatabase) { - database.query("SELECT * FROM Messages").use { + private fun migrateMessages(db: SupportSQLiteDatabase) { + db.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") + db.execSQL("UPDATE Messages SET date = $timestampUtc WHERE id = $id") } } } - private fun migrateMobileDevices(database: SupportSQLiteDatabase) { - database.query("SELECT * FROM MobileDevices").use { + private fun migrateMobileDevices(db: SupportSQLiteDatabase) { + db.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") + db.execSQL("UPDATE MobileDevices SET date = $timestampUtc WHERE id = $id") } } } - private fun migrateNotifications(database: SupportSQLiteDatabase) { - database.query("SELECT * FROM Notifications").use { + private fun migrateNotifications(db: SupportSQLiteDatabase) { + db.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") + db.execSQL("UPDATE Notifications SET date = $timestampUtc WHERE id = $id") } } } - private fun migrateTimetable(database: SupportSQLiteDatabase) { - database.query("SELECT * FROM Timetable").use { + private fun migrateTimetable(db: SupportSQLiteDatabase) { + db.query("SELECT * FROM Timetable").use { while (it.moveToNext()) { val id = it.getLong(it.getColumnIndexOrThrow("id")) val timestampLocalStart = it.getLong(it.getColumnIndexOrThrow("start")) @@ -74,13 +74,13 @@ class Migration46 : Migration(45, 46) { val timestampUtcStart = timestampLocalStart.timestampLocalToUTC() val timestampUtcEnd = timestampLocalEnd.timestampLocalToUTC() - database.execSQL("UPDATE Timetable SET start = $timestampUtcStart, end = $timestampUtcEnd WHERE id = $id") + db.execSQL("UPDATE Timetable SET start = $timestampUtcStart, end = $timestampUtcEnd WHERE id = $id") } } } - private fun migrateTimetableAdditional(database: SupportSQLiteDatabase) { - database.query("SELECT * FROM TimetableAdditional").use { + private fun migrateTimetableAdditional(db: SupportSQLiteDatabase) { + db.query("SELECT * FROM TimetableAdditional").use { while (it.moveToNext()) { val id = it.getLong(it.getColumnIndexOrThrow("id")) val timestampLocalStart = it.getLong(it.getColumnIndexOrThrow("start")) @@ -88,7 +88,7 @@ class Migration46 : Migration(45, 46) { val timestampUtcStart = timestampLocalStart.timestampLocalToUTC() val timestampUtcEnd = timestampLocalEnd.timestampLocalToUTC() - database.execSQL("UPDATE TimetableAdditional SET start = $timestampUtcStart, end = $timestampUtcEnd WHERE id = $id") + db.execSQL("UPDATE TimetableAdditional SET start = $timestampUtcStart, end = $timestampUtcEnd WHERE id = $id") } } } 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 index 6e1de19d4..97766c01e 100644 --- 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 @@ -5,10 +5,10 @@ import androidx.sqlite.db.SupportSQLiteDatabase class Migration49 : Migration(48, 49) { - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("DROP TABLE IF EXISTS SchoolAnnouncements") + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("DROP TABLE IF EXISTS SchoolAnnouncements") - database.execSQL( + db.execSQL( """ CREATE TABLE IF NOT EXISTS `SchoolAnnouncements` ( `user_login_id` INTEGER NOT NULL, diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration5.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration5.kt index dbcd916ba..a5b4e8e1a 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration5.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration5.kt @@ -7,11 +7,16 @@ import java.time.ZoneOffset class Migration5 : Migration(4, 5) { - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("ALTER TABLE Students ADD COLUMN registration_date INTEGER DEFAULT 0 NOT NULL") - database.execSQL("UPDATE Students SET registration_date = '${now().atZone(ZoneOffset.UTC).toInstant().toEpochMilli()}'") - database.execSQL("DROP TABLE IF EXISTS Notes") - database.execSQL(""" + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("ALTER TABLE Students ADD COLUMN registration_date INTEGER DEFAULT 0 NOT NULL") + db.execSQL( + "UPDATE Students SET registration_date = '${ + now().atZone(ZoneOffset.UTC).toInstant().toEpochMilli() + }'" + ) + db.execSQL("DROP TABLE IF EXISTS Notes") + db.execSQL( + """ CREATE TABLE IF NOT EXISTS Notes ( id INTEGER PRIMARY KEY NOT NULL, is_read INTEGER NOT NULL, @@ -21,6 +26,7 @@ class Migration5 : Migration(4, 5) { teacher TEXT NOT NULL, category TEXT NOT NULL, content TEXT NOT NULL) - """) + """ + ) } } 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 index d45a81570..577998ca0 100644 --- 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 @@ -5,9 +5,9 @@ import androidx.sqlite.db.SupportSQLiteDatabase class Migration50 : Migration(49, 50) { - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("DROP TABLE IF EXISTS MobileDevices") - database.execSQL( + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("DROP TABLE IF EXISTS MobileDevices") + db.execSQL( """ CREATE TABLE IF NOT EXISTS `MobileDevices` ( `user_login_id` INTEGER NOT NULL, 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 index e78e2e3a7..7023049f9 100644 --- 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 @@ -5,17 +5,17 @@ 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) + override fun migrate(db: SupportSQLiteDatabase) { + createMailboxTable(db) + recreateMessagesTable(db) + recreateMessageAttachmentsTable(db) + recreateRecipientsTable(db) + deleteReportingUnitTable(db) } - private fun createMailboxTable(database: SupportSQLiteDatabase) { - database.execSQL("DROP TABLE IF EXISTS Mailboxes") - database.execSQL( + private fun createMailboxTable(db: SupportSQLiteDatabase) { + db.execSQL("DROP TABLE IF EXISTS Mailboxes") + db.execSQL( """ CREATE TABLE IF NOT EXISTS `Mailboxes` ( `globalKey` TEXT NOT NULL, @@ -30,9 +30,9 @@ class Migration51 : Migration(50, 51) { ) } - private fun recreateMessagesTable(database: SupportSQLiteDatabase) { - database.execSQL("DROP TABLE IF EXISTS Messages") - database.execSQL( + private fun recreateMessagesTable(db: SupportSQLiteDatabase) { + db.execSQL("DROP TABLE IF EXISTS Messages") + db.execSQL( """ CREATE TABLE IF NOT EXISTS `Messages` ( `message_global_key` TEXT NOT NULL, @@ -52,9 +52,9 @@ class Migration51 : Migration(50, 51) { ) } - private fun recreateMessageAttachmentsTable(database: SupportSQLiteDatabase) { - database.execSQL("DROP TABLE IF EXISTS MessageAttachments") - database.execSQL( + private fun recreateMessageAttachmentsTable(db: SupportSQLiteDatabase) { + db.execSQL("DROP TABLE IF EXISTS MessageAttachments") + db.execSQL( """ CREATE TABLE IF NOT EXISTS `MessageAttachments` ( `real_id` INTEGER NOT NULL, @@ -66,9 +66,9 @@ class Migration51 : Migration(50, 51) { ) } - private fun recreateRecipientsTable(database: SupportSQLiteDatabase) { - database.execSQL("DROP TABLE IF EXISTS Recipients") - database.execSQL( + private fun recreateRecipientsTable(db: SupportSQLiteDatabase) { + db.execSQL("DROP TABLE IF EXISTS Recipients") + db.execSQL( """ CREATE TABLE IF NOT EXISTS `Recipients` ( `mailboxGlobalKey` TEXT NOT NULL, @@ -82,7 +82,7 @@ class Migration51 : Migration(50, 51) { ) } - private fun deleteReportingUnitTable(database: SupportSQLiteDatabase) { - database.execSQL("DROP TABLE IF EXISTS ReportingUnits") + private fun deleteReportingUnitTable(db: SupportSQLiteDatabase) { + db.execSQL("DROP TABLE IF EXISTS ReportingUnits") } } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration53.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration53.kt new file mode 100644 index 000000000..dd9e68c97 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration53.kt @@ -0,0 +1,57 @@ +package io.github.wulkanowy.data.db.migrations + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +class Migration53 : Migration(52, 53) { + + override fun migrate(db: SupportSQLiteDatabase) { + createMailboxTable(db) + recreateMessagesTable(db) + } + + private fun createMailboxTable(db: SupportSQLiteDatabase) { + db.execSQL("DROP TABLE IF EXISTS Mailboxes") + db.execSQL( + """ + CREATE TABLE IF NOT EXISTS `Mailboxes` ( + `globalKey` TEXT NOT NULL, + `email` TEXT NOT NULL, + `symbol` TEXT NOT NULL, + `schoolId` TEXT NOT NULL, + `fullName` TEXT NOT NULL, + `userName` TEXT NOT NULL, + `studentName` TEXT NOT NULL, + `schoolNameShort` TEXT NOT NULL, + `type` TEXT NOT NULL, + PRIMARY KEY(`globalKey`) + )""".trimIndent() + ) + } + + private fun recreateMessagesTable(db: SupportSQLiteDatabase) { + db.execSQL("DROP TABLE IF EXISTS Messages") + db.execSQL( + """ + CREATE TABLE IF NOT EXISTS `Messages` ( + `email` TEXT NOT NULL, + `message_global_key` TEXT NOT NULL, + `mailbox_key` TEXT NOT NULL, + `message_id` INTEGER NOT NULL, + `correspondents` TEXT NOT NULL, + `subject` TEXT NOT NULL, + `date` INTEGER NOT NULL, + `folder_id` INTEGER NOT NULL, + `unread` INTEGER NOT NULL, + `read_by` INTEGER, + `unread_by` INTEGER, + `has_attachments` INTEGER NOT NULL, + `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + `is_notified` INTEGER NOT NULL, + `content` TEXT NOT NULL, + `sender` TEXT, + `recipients` TEXT + )""".trimIndent() + ) + } +} diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration54.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration54.kt new file mode 100644 index 000000000..60bd21f0a --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration54.kt @@ -0,0 +1,28 @@ +package io.github.wulkanowy.data.db.migrations + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +class Migration54 : Migration(53, 54) { + + override fun migrate(db: SupportSQLiteDatabase) { + migrateResman(db) + removeTomaszowMazowieckiStudents(db) + } + + private fun migrateResman(db: SupportSQLiteDatabase) { + db.execSQL( + """ + UPDATE Students SET + scrapper_base_url = 'https://vulcan.net.pl', + login_type = 'ADFSLightScoped', + symbol = 'rzeszowprojekt' + WHERE scrapper_base_url = 'https://resman.pl' + """.trimIndent() + ) + } + + private fun removeTomaszowMazowieckiStudents(db: SupportSQLiteDatabase) { + db.execSQL("DELETE FROM Students WHERE symbol = 'tomaszowmazowiecki'") + } +} diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration55.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration55.kt new file mode 100644 index 000000000..424be171b --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration55.kt @@ -0,0 +1,17 @@ +package io.github.wulkanowy.data.db.migrations + +import androidx.room.DeleteColumn +import androidx.room.migration.AutoMigrationSpec +import androidx.sqlite.db.SupportSQLiteDatabase + +@DeleteColumn( + tableName = "MessageAttachments", + columnName = "real_id", +) +class Migration55 : AutoMigrationSpec { + + override fun onPostMigrate(db: SupportSQLiteDatabase) { + db.execSQL("DELETE FROM Messages") + db.execSQL("DELETE FROM MessageAttachments") + } +} diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration57.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration57.kt new file mode 100644 index 000000000..2fc8718f9 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration57.kt @@ -0,0 +1,10 @@ +package io.github.wulkanowy.data.db.migrations + +import androidx.room.DeleteColumn +import androidx.room.migration.AutoMigrationSpec + +@DeleteColumn( + tableName = "AdminMessages", + columnName = "type", +) +class Migration57 : AutoMigrationSpec diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration58.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration58.kt new file mode 100644 index 000000000..c440d58d6 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration58.kt @@ -0,0 +1,10 @@ +package io.github.wulkanowy.data.db.migrations + +import androidx.room.DeleteColumn +import androidx.room.migration.AutoMigrationSpec + +@DeleteColumn( + tableName = "AdminMessages", + columnName = "is_dismissible", +) +class Migration58 : AutoMigrationSpec diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration6.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration6.kt index fa9436187..06cd5f0fd 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration6.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration6.kt @@ -5,8 +5,9 @@ import androidx.sqlite.db.SupportSQLiteDatabase class Migration6 : Migration(5, 6) { - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL(""" + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL( + """ CREATE TABLE IF NOT EXISTS ReportingUnits ( id INTEGER PRIMARY KEY NOT NULL, student_id INTEGER NOT NULL, @@ -15,9 +16,11 @@ class Migration6 : Migration(5, 6) { sender_id INTEGER NOT NULL, sender_name TEXT NOT NULL, roles TEXT NOT NULL) - """) + """ + ) - database.execSQL(""" + db.execSQL( + """ CREATE TABLE IF NOT EXISTS Recipients ( id INTEGER PRIMARY KEY NOT NULL, student_id INTEGER NOT NULL, @@ -28,10 +31,11 @@ class Migration6 : Migration(5, 6) { unit_id INTEGER NOT NULL, role INTEGER NOT NULL, hash TEXT NOT NULL) - """) + """ + ) - database.execSQL("DELETE FROM Semesters WHERE 1") - database.execSQL("ALTER TABLE Semesters ADD COLUMN class_id INTEGER DEFAULT 0 NOT NULL") - database.execSQL("ALTER TABLE Semesters ADD COLUMN unit_id INTEGER DEFAULT 0 NOT NULL") + db.execSQL("DELETE FROM Semesters WHERE 1") + db.execSQL("ALTER TABLE Semesters ADD COLUMN class_id INTEGER DEFAULT 0 NOT NULL") + db.execSQL("ALTER TABLE Semesters ADD COLUMN unit_id INTEGER DEFAULT 0 NOT NULL") } } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration7.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration7.kt index 120716c81..83a822f22 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration7.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration7.kt @@ -5,8 +5,9 @@ import androidx.sqlite.db.SupportSQLiteDatabase class Migration7 : Migration(6, 7) { - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL(""" + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL( + """ CREATE TABLE IF NOT EXISTS GradesStatistics ( id INTEGER PRIMARY KEY NOT NULL, student_id INTEGER NOT NULL, @@ -15,6 +16,7 @@ class Migration7 : Migration(6, 7) { grade INTEGER NOT NULL, amount INTEGER NOT NULL, is_semester INTEGER NOT NULL) - """) + """ + ) } } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration8.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration8.kt index 7009ee129..992e8c68d 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration8.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration8.kt @@ -5,9 +5,9 @@ import androidx.sqlite.db.SupportSQLiteDatabase class Migration8 : Migration(7, 8) { - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("ALTER TABLE Timetable ADD COLUMN subjectOld TEXT DEFAULT \"\" NOT NULL") - database.execSQL("ALTER TABLE Timetable ADD COLUMN roomOld TEXT DEFAULT \"\" NOT NULL") - database.execSQL("ALTER TABLE Timetable ADD COLUMN teacherOld TEXT DEFAULT \"\" NOT NULL") + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("ALTER TABLE Timetable ADD COLUMN subjectOld TEXT DEFAULT \"\" NOT NULL") + db.execSQL("ALTER TABLE Timetable ADD COLUMN roomOld TEXT DEFAULT \"\" NOT NULL") + db.execSQL("ALTER TABLE Timetable ADD COLUMN teacherOld TEXT DEFAULT \"\" NOT NULL") } } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration9.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration9.kt index d79a57062..b83c34c41 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration9.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration9.kt @@ -5,9 +5,10 @@ import androidx.sqlite.db.SupportSQLiteDatabase class Migration9 : Migration(8, 9) { - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("DROP TABLE IF EXISTS Messages") - database.execSQL(""" + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("DROP TABLE IF EXISTS Messages") + db.execSQL( + """ CREATE TABLE IF NOT EXISTS Messages ( id INTEGER PRIMARY KEY NOT NULL, student_id INTEGER NOT NULL, diff --git a/app/src/main/java/io/github/wulkanowy/data/enums/MessageType.kt b/app/src/main/java/io/github/wulkanowy/data/enums/MessageType.kt new file mode 100644 index 000000000..531684e4e --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/enums/MessageType.kt @@ -0,0 +1,9 @@ +package io.github.wulkanowy.data.enums + +enum class MessageType { + GENERAL_MESSAGE, + DASHBOARD_MESSAGE, + LOGIN_MESSAGE, + PASS_RESET_MESSAGE, + ERROR_OVERRIDE, +} diff --git a/app/src/main/java/io/github/wulkanowy/data/enums/TimetableGapsMode.kt b/app/src/main/java/io/github/wulkanowy/data/enums/TimetableGapsMode.kt new file mode 100644 index 000000000..c8310c02d --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/enums/TimetableGapsMode.kt @@ -0,0 +1,11 @@ +package io.github.wulkanowy.data.enums + +enum class TimetableGapsMode(val value: String) { + NO_GAPS("no_gaps"), + BETWEEN_LESSONS("between"), + BETWEEN_AND_BEFORE_LESSONS("before_and_between"); + + companion object { + fun getByValue(value: String) = entries.find { it.value == value } ?: BETWEEN_LESSONS + } +} diff --git a/app/src/main/java/io/github/wulkanowy/data/mappers/AttendanceMapper.kt b/app/src/main/java/io/github/wulkanowy/data/mappers/AttendanceMapper.kt index 46e67fdaa..c0ed0c8c2 100644 --- a/app/src/main/java/io/github/wulkanowy/data/mappers/AttendanceMapper.kt +++ b/app/src/main/java/io/github/wulkanowy/data/mappers/AttendanceMapper.kt @@ -3,17 +3,22 @@ package io.github.wulkanowy.data.mappers import io.github.wulkanowy.data.db.entities.Attendance import io.github.wulkanowy.data.db.entities.AttendanceSummary import io.github.wulkanowy.data.db.entities.Semester +import io.github.wulkanowy.data.db.entities.Timetable import io.github.wulkanowy.sdk.pojo.Attendance as SdkAttendance import io.github.wulkanowy.sdk.pojo.AttendanceSummary as SdkAttendanceSummary -fun List.mapToEntities(semester: Semester) = map { +fun List.mapToEntities(semester: Semester, lessons: List) = map { Attendance( studentId = semester.studentId, diaryId = semester.diaryId, date = it.date, timeId = it.timeId, number = it.number, - subject = it.subject, + subject = it.subject.ifBlank { + lessons.find { lesson -> + lesson.date == it.date && lesson.number == it.number + }?.subject.orEmpty() + }, name = it.name, presence = it.presence, absence = it.absence, 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 17a9e5cdb..add6439d4 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,9 +10,9 @@ fun List.mapToEntities(semester: Semester) = map { diaryId = semester.diaryId, agenda = it.agenda, conferenceId = it.id, - date = it.dateZoned.toInstant(), + date = it.date.toInstant(), presentOnConference = it.presentOnConference, - subject = it.subject, - title = it.title + subject = it.topic, + title = it.place, ) } diff --git a/app/src/main/java/io/github/wulkanowy/data/mappers/ExamMapper.kt b/app/src/main/java/io/github/wulkanowy/data/mappers/ExamMapper.kt index bdb5efbba..173dfebf9 100644 --- a/app/src/main/java/io/github/wulkanowy/data/mappers/ExamMapper.kt +++ b/app/src/main/java/io/github/wulkanowy/data/mappers/ExamMapper.kt @@ -11,7 +11,7 @@ fun List.mapToEntities(semester: Semester) = map { date = it.date, entryDate = it.entryDate, subject = it.subject, - group = it.group, + group = "", type = it.type, description = it.description, teacher = it.teacher, diff --git a/app/src/main/java/io/github/wulkanowy/data/mappers/GradeMapper.kt b/app/src/main/java/io/github/wulkanowy/data/mappers/GradeMapper.kt index 178de682a..66e922171 100644 --- a/app/src/main/java/io/github/wulkanowy/data/mappers/GradeMapper.kt +++ b/app/src/main/java/io/github/wulkanowy/data/mappers/GradeMapper.kt @@ -1,10 +1,12 @@ package io.github.wulkanowy.data.mappers import io.github.wulkanowy.data.db.entities.Grade +import io.github.wulkanowy.data.db.entities.GradeDescriptive import io.github.wulkanowy.data.db.entities.GradeSummary import io.github.wulkanowy.data.db.entities.Semester -import io.github.wulkanowy.sdk.pojo.GradeSummary as SdkGradeSummary import io.github.wulkanowy.sdk.pojo.Grade as SdkGrade +import io.github.wulkanowy.sdk.pojo.GradeDescriptive as SdkGradeDescriptive +import io.github.wulkanowy.sdk.pojo.GradeSummary as SdkGradeSummary fun List.mapToEntities(semester: Semester) = map { Grade( @@ -40,3 +42,15 @@ fun List.mapToEntities(semester: Semester) = map { average = it.average ) } + +@JvmName("mapGradeDescriptiveToEntities") +fun List.mapToEntities(semester: Semester) = map { + GradeDescriptive( + semesterId = semester.semesterId, + studentId = semester.studentId, + subject = it.subject, + description = it.description + ) +} + + diff --git a/app/src/main/java/io/github/wulkanowy/data/mappers/MailboxMapper.kt b/app/src/main/java/io/github/wulkanowy/data/mappers/MailboxMapper.kt index 2ccca1b90..0cf547770 100644 --- a/app/src/main/java/io/github/wulkanowy/data/mappers/MailboxMapper.kt +++ b/app/src/main/java/io/github/wulkanowy/data/mappers/MailboxMapper.kt @@ -10,9 +10,11 @@ fun List.mapToEntities(student: Student) = map { globalKey = it.globalKey, fullName = it.fullName, userName = it.userName, - userLoginId = student.userLoginId, studentName = it.studentName, schoolNameShort = it.schoolNameShort, type = MailboxType.valueOf(it.type.name), + email = student.email, + symbol = student.symbol, + schoolId = student.schoolSymbol, ) } 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 2e7967f0e..a26d76651 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 @@ -2,21 +2,36 @@ package io.github.wulkanowy.data.mappers import io.github.wulkanowy.data.db.entities.* import io.github.wulkanowy.sdk.pojo.MailboxType +import timber.log.Timber 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(mailbox: Mailbox) = map { +fun List.mapToEntities( + student: Student, + mailbox: Mailbox?, + allMailboxes: List +): List = map { Message( messageGlobalKey = it.globalKey, - mailboxKey = mailbox.globalKey, + mailboxKey = mailbox?.globalKey ?: allMailboxes.find { box -> + box.fullName == it.mailbox + }?.globalKey.let { mailboxKey -> + if (mailboxKey == null) { + Timber.e("Can't find ${it.mailbox} in $allMailboxes") + "unknown" + } else mailboxKey + }, + email = student.email, messageId = it.id, correspondents = it.correspondents, subject = it.subject.trim(), - date = it.dateZoned.toInstant(), + date = it.date.toInstant(), folderId = it.folderId, unread = it.unread, - hasAttachments = it.hasAttachments + unreadBy = it.unreadBy, + readBy = it.readBy, + hasAttachments = it.hasAttachments, ).apply { content = it.content.orEmpty() } @@ -25,7 +40,6 @@ fun List.mapToEntities(mailbox: Mailbox) = map { fun List.mapToEntities(messageGlobalKey: String) = map { MessageAttachment( messageGlobalKey = messageGlobalKey, - realId = it.url.hashCode(), url = it.url, filename = it.filename ) 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 1a1c501f6..1f4178fae 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 @@ -9,7 +9,7 @@ import io.github.wulkanowy.sdk.pojo.Token as SdkToken fun List.mapToEntities(student: Student) = map { MobileDevice( userLoginId = student.userLoginId, - date = it.createDateZoned.toInstant(), + date = it.createDate.toInstant(), deviceId = it.id, name = it.name ) diff --git a/app/src/main/java/io/github/wulkanowy/data/mappers/RegisterUserMapper.kt b/app/src/main/java/io/github/wulkanowy/data/mappers/RegisterUserMapper.kt new file mode 100644 index 000000000..72c4861c9 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/mappers/RegisterUserMapper.kt @@ -0,0 +1,90 @@ +package io.github.wulkanowy.data.mappers + +import io.github.wulkanowy.data.db.entities.Student +import io.github.wulkanowy.data.db.entities.StudentWithSemesters +import io.github.wulkanowy.data.pojos.* +import java.time.Instant +import io.github.wulkanowy.sdk.pojo.RegisterStudent as SdkRegisterStudent +import io.github.wulkanowy.sdk.pojo.RegisterUser as SdkRegisterUser + +fun SdkRegisterUser.mapToPojo(password: String?) = RegisterUser( + email = email, + login = login, + password = password, + scrapperBaseUrl = scrapperBaseUrl, + loginMode = loginMode, + loginType = loginType, + symbols = symbols.map { registerSymbol -> + RegisterSymbol( + symbol = registerSymbol.symbol, + error = registerSymbol.error, + hebeBaseUrl = registerSymbol.hebeBaseUrl, + keyId = registerSymbol.keyId, + privatePem = registerSymbol.privatePem, + userName = registerSymbol.userName, + schools = registerSymbol.schools.map { + RegisterUnit( + userLoginId = it.userLoginId, + schoolId = it.schoolId, + schoolName = it.schoolName, + schoolShortName = it.schoolShortName, + parentIds = it.parentIds, + studentIds = it.studentIds, + employeeIds = it.employeeIds, + error = it.error, + students = it.subjects + .filterIsInstance() + .map { registerSubject -> + RegisterStudent( + studentId = registerSubject.studentId, + studentName = registerSubject.studentName, + studentSecondName = registerSubject.studentSecondName, + studentSurname = registerSubject.studentSurname, + className = registerSubject.className, + classId = registerSubject.classId, + isParent = registerSubject.isParent, + semesters = registerSubject.semesters + .mapToEntities(registerSubject.studentId), + ) + }, + ) + } + ) + }, +) + +fun RegisterStudent.mapToStudentWithSemesters( + user: RegisterUser, + scrapperDomainSuffix: String, + symbol: RegisterSymbol, + unit: RegisterUnit, + colors: List, +): StudentWithSemesters = StudentWithSemesters( + semesters = semesters, + student = Student( + email = user.login, // for compatibility + userName = symbol.userName, + userLoginId = unit.userLoginId, + isParent = isParent, + className = className, + classId = classId, + studentId = studentId, + symbol = symbol.symbol, + loginType = user.loginType?.name.orEmpty(), + schoolName = unit.schoolName, + schoolShortName = unit.schoolShortName, + schoolSymbol = unit.schoolId, + studentName = "$studentName $studentSurname", + loginMode = user.loginMode.name, + scrapperBaseUrl = user.scrapperBaseUrl.orEmpty(), + scrapperDomainSuffix = scrapperDomainSuffix, + mobileBaseUrl = symbol.hebeBaseUrl.orEmpty(), + certificateKey = symbol.keyId.orEmpty(), + privateKey = symbol.privatePem.orEmpty(), + password = user.password.orEmpty(), + isCurrent = false, + registrationDate = Instant.now(), + ).apply { + avatarColor = colors.random() + }, +) 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 deleted file mode 100644 index a2110d7f5..000000000 --- a/app/src/main/java/io/github/wulkanowy/data/mappers/StudentMapper.kt +++ /dev/null @@ -1,37 +0,0 @@ -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.Instant -import io.github.wulkanowy.sdk.pojo.Student as SdkStudent - -fun List.mapToEntities(password: String = "", colors: List) = map { - StudentWithSemesters( - student = Student( - email = it.email, - password = password, - isParent = it.isParent, - symbol = it.symbol, - studentId = it.studentId, - userLoginId = it.userLoginId, - userName = it.userName, - studentName = it.studentName + " " + it.studentSurname, - schoolSymbol = it.schoolSymbol, - schoolShortName = it.schoolShortName, - schoolName = it.schoolName, - className = it.className, - classId = it.classId, - scrapperBaseUrl = it.scrapperBaseUrl, - loginType = it.loginType.name, - isCurrent = false, - registrationDate = Instant.now(), - mobileBaseUrl = it.mobileBaseUrl, - privateKey = it.privateKey, - certificateKey = it.certificateKey, - loginMode = it.loginMode.name, - ).apply { - avatarColor = colors.random() - }, - semesters = it.semesters.mapToEntities(it.studentId) - ) -} 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 e55aa3cf7..ee525e108 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 @@ -5,10 +5,10 @@ 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.pojos.TimetableFull -import io.github.wulkanowy.sdk.pojo.TimetableFull as SdkTimetableFull +import io.github.wulkanowy.sdk.pojo.Timetable as SdkTimetableFull import io.github.wulkanowy.sdk.pojo.TimetableDayHeader as SdkTimetableHeader -import io.github.wulkanowy.sdk.pojo.Timetable as SdkTimetable -import io.github.wulkanowy.sdk.pojo.TimetableAdditional as SdkTimetableAdditional +import io.github.wulkanowy.sdk.pojo.Lesson as SdkLesson +import io.github.wulkanowy.sdk.pojo.LessonAdditional as SdkTimetableAdditional fun SdkTimetableFull.mapToEntities(semester: Semester) = TimetableFull( lessons = lessons.mapToEntities(semester), @@ -16,13 +16,13 @@ fun SdkTimetableFull.mapToEntities(semester: Semester) = TimetableFull( headers = headers.mapToEntities(semester) ) -fun List.mapToEntities(semester: Semester) = map { +fun List.mapToEntities(semester: Semester) = map { Timetable( studentId = semester.studentId, diaryId = semester.diaryId, number = it.number, - start = it.startZoned.toInstant(), - end = it.endZoned.toInstant(), + start = it.start.toInstant(), + end = it.end.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.startZoned.toInstant(), - end = it.endZoned.toInstant(), + start = it.start.toInstant(), + end = it.end.toInstant(), ) } diff --git a/app/src/main/java/io/github/wulkanowy/data/pojos/LoginEvent.kt b/app/src/main/java/io/github/wulkanowy/data/pojos/LoginEvent.kt new file mode 100644 index 000000000..c2b4d2ded --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/pojos/LoginEvent.kt @@ -0,0 +1,21 @@ +package io.github.wulkanowy.data.pojos + +import kotlinx.serialization.Serializable + +@Serializable +data class LoginEvent( + val uuid: String, + val schoolName: String, + val schoolShort: String, + val schoolAddress: String, + val scraperBaseUrl: String, + val symbol: String, + val schoolId: String, + val loginType: String, +) + +@Serializable +data class IntegrityRequest( + val tokenString: String, + val data: T, +) diff --git a/app/src/main/java/io/github/wulkanowy/data/pojos/RegisterUser.kt b/app/src/main/java/io/github/wulkanowy/data/pojos/RegisterUser.kt new file mode 100644 index 000000000..98bf1402b --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/pojos/RegisterUser.kt @@ -0,0 +1,48 @@ +package io.github.wulkanowy.data.pojos + +import io.github.wulkanowy.data.db.entities.Semester +import io.github.wulkanowy.sdk.Sdk +import io.github.wulkanowy.sdk.scrapper.Scrapper + +data class RegisterUser( + val email: String, + val password: String?, + val login: String, // may be the same as email + val scrapperBaseUrl: String?, + val loginType: Scrapper.LoginType?, + val loginMode: Sdk.Mode, + val symbols: List, +) : java.io.Serializable + +data class RegisterSymbol( + val symbol: String, + val error: Throwable?, + val hebeBaseUrl: String?, + val keyId: String?, + val privatePem: String?, + val userName: String, + val schools: List, +) : java.io.Serializable + +data class RegisterUnit( + val userLoginId: Int, + val schoolId: String, + val schoolName: String, + val schoolShortName: String, + val parentIds: List, + val studentIds: List, + val employeeIds: List, + val error: Throwable?, + val students: List, +) : java.io.Serializable + +data class RegisterStudent( + val studentId: Int, + val studentName: String, + val studentSecondName: String, + val studentSurname: String, + val className: String, + val classId: Int, + val isParent: Boolean, + val semesters: List, +) : java.io.Serializable 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 c9655b722..b831ee755 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 @@ -1,10 +1,11 @@ package io.github.wulkanowy.data.repositories +import io.github.wulkanowy.data.Resource import io.github.wulkanowy.data.api.AdminMessageService import io.github.wulkanowy.data.db.dao.AdminMessageDao -import io.github.wulkanowy.data.db.entities.Student +import io.github.wulkanowy.data.db.entities.AdminMessage import io.github.wulkanowy.data.networkBoundResource -import io.github.wulkanowy.utils.AppInfo +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.sync.Mutex import javax.inject.Inject import javax.inject.Singleton @@ -13,34 +14,20 @@ import javax.inject.Singleton class AdminMessageRepository @Inject constructor( private val adminMessageService: AdminMessageService, private val adminMessageDao: AdminMessageDao, - private val appInfo: AppInfo ) { + private val saveFetchResultMutex = Mutex() - suspend fun getAdminMessages(student: Student) = networkBoundResource( - mutex = saveFetchResultMutex, - isResultEmpty = { it == null }, - query = { adminMessageDao.loadAll() }, - fetch = { adminMessageService.getAdminMessages() }, - shouldFetch = { true }, - saveFetchResult = { oldItems, newItems -> - adminMessageDao.removeOldAndSaveNew(oldItems, newItems) - }, - showSavedOnLoading = false, - mapResult = { adminMessages -> - adminMessages.filter { adminMessage -> - val isCorrectRegister = adminMessage.targetRegisterHost?.let { - student.scrapperBaseUrl.contains(it, true) - } ?: true - val isCorrectFlavor = - adminMessage.targetFlavor?.equals(appInfo.buildFlavor, true) ?: true - val isCorrectMaxVersion = - adminMessage.versionMax?.let { it >= appInfo.versionCode } ?: true - val isCorrectMinVersion = - adminMessage.versionMin?.let { it <= appInfo.versionCode } ?: true - - isCorrectRegister && isCorrectFlavor && isCorrectMaxVersion && isCorrectMinVersion - }.maxByOrNull { it.id } - } - ) + fun getAdminMessages(): Flow>> = + networkBoundResource( + mutex = saveFetchResultMutex, + isResultEmpty = { false }, + query = { adminMessageDao.loadAll() }, + fetch = { adminMessageService.getAdminMessages() }, + shouldFetch = { true }, + saveFetchResult = { oldItems, newItems -> + adminMessageDao.removeOldAndSaveNew(oldItems, newItems) + }, + showSavedOnLoading = false, + ) } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/AppCreatorRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/AppCreatorRepository.kt index cbaa12bd3..bec2797db 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/AppCreatorRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/AppCreatorRepository.kt @@ -19,7 +19,6 @@ class AppCreatorRepository @Inject constructor( ) { @OptIn(ExperimentalSerializationApi::class) - @Suppress("BlockingMethodInNonBlockingContext") suspend fun getAppCreators() = withContext(dispatchers.io) { val inputStream = context.assets.open("contributors.json").buffered() json.decodeFromStream>(inputStream) diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceRepository.kt index 9aa6562a6..6d782047b 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceRepository.kt @@ -1,6 +1,7 @@ package io.github.wulkanowy.data.repositories import io.github.wulkanowy.data.db.dao.AttendanceDao +import io.github.wulkanowy.data.db.dao.TimetableDao import io.github.wulkanowy.data.db.entities.Attendance import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student @@ -8,9 +9,17 @@ 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.* +import io.github.wulkanowy.utils.AutoRefreshHelper +import io.github.wulkanowy.utils.getRefreshKey +import io.github.wulkanowy.utils.init +import io.github.wulkanowy.utils.monday +import io.github.wulkanowy.utils.sunday +import io.github.wulkanowy.utils.switchSemester +import io.github.wulkanowy.utils.uniqueSubtract +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.withContext import java.time.LocalDate import java.time.LocalDateTime import java.time.LocalTime @@ -20,6 +29,7 @@ import javax.inject.Singleton @Singleton class AttendanceRepository @Inject constructor( private val attendanceDb: AttendanceDao, + private val timetableDb: TimetableDao, private val sdk: Sdk, private val refreshHelper: AutoRefreshHelper, ) { @@ -48,10 +58,15 @@ class AttendanceRepository @Inject constructor( attendanceDb.loadAll(semester.diaryId, semester.studentId, start.monday, end.sunday) }, fetch = { + val lessons = withContext(Dispatchers.IO) { + timetableDb.load( + semester.diaryId, semester.studentId, start.monday, end.sunday + ) + } sdk.init(student) - .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) - .getAttendance(start.monday, end.sunday, semester.semesterId) - .mapToEntities(semester) + .switchSemester(semester) + .getAttendance(start.monday, end.sunday) + .mapToEntities(semester, lessons) }, saveFetchResult = { old, new -> attendanceDb.deleteAll(old uniqueSubtract new) @@ -88,7 +103,7 @@ class AttendanceRepository @Inject constructor( ) } sdk.init(student) - .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) + .switchSemester(semester) .excuseForAbsence(items, reason) } } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceSummaryRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceSummaryRepository.kt index 8e0709135..6bdcf9d7f 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceSummaryRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceSummaryRepository.kt @@ -9,6 +9,7 @@ import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.AutoRefreshHelper import io.github.wulkanowy.utils.getRefreshKey import io.github.wulkanowy.utils.init +import io.github.wulkanowy.utils.switchSemester import io.github.wulkanowy.utils.uniqueSubtract import kotlinx.coroutines.sync.Mutex import javax.inject.Inject @@ -40,7 +41,7 @@ class AttendanceSummaryRepository @Inject constructor( query = { attendanceDb.loadAll(semester.diaryId, semester.studentId, subjectId) }, fetch = { sdk.init(student) - .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) + .switchSemester(semester) .getAttendanceSummary(subjectId) .mapToEntities(semester, subjectId) }, diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/CompletedLessonsRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/CompletedLessonsRepository.kt index 8f393cadb..1579ae62b 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/CompletedLessonsRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/CompletedLessonsRepository.kt @@ -48,7 +48,7 @@ class CompletedLessonsRepository @Inject constructor( }, fetch = { sdk.init(student) - .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) + .switchSemester(semester) .getCompletedLessons(start.monday, end.sunday) .mapToEntities(semester) }, diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/ConferenceRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/ConferenceRepository.kt index 83204cab0..7eb37f0b7 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/ConferenceRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/ConferenceRepository.kt @@ -10,6 +10,7 @@ import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.AutoRefreshHelper import io.github.wulkanowy.utils.getRefreshKey import io.github.wulkanowy.utils.init +import io.github.wulkanowy.utils.switchSemester import io.github.wulkanowy.utils.uniqueSubtract import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.sync.Mutex @@ -46,7 +47,7 @@ class ConferenceRepository @Inject constructor( }, fetch = { sdk.init(student) - .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) + .switchSemester(semester) .getConferences() .mapToEntities(semester) .filter { it.date >= startDate } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/ExamRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/ExamRepository.kt index faa80b93e..96026a55b 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/ExamRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/ExamRepository.kt @@ -7,7 +7,13 @@ import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.data.networkBoundResource import io.github.wulkanowy.sdk.Sdk -import io.github.wulkanowy.utils.* +import io.github.wulkanowy.utils.AutoRefreshHelper +import io.github.wulkanowy.utils.endExamsDay +import io.github.wulkanowy.utils.getRefreshKey +import io.github.wulkanowy.utils.init +import io.github.wulkanowy.utils.startExamsDay +import io.github.wulkanowy.utils.switchSemester +import io.github.wulkanowy.utils.uniqueSubtract import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.sync.Mutex import java.time.LocalDate @@ -51,8 +57,8 @@ class ExamRepository @Inject constructor( }, fetch = { sdk.init(student) - .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) - .getExams(start.startExamsDay, start.endExamsDay, semester.semesterId) + .switchSemester(semester) + .getExams(start.startExamsDay, start.endExamsDay) .mapToEntities(semester) }, saveFetchResult = { old, new -> @@ -67,14 +73,16 @@ class ExamRepository @Inject constructor( filterResult = { it.filter { item -> item.date in start..end } } ) - fun getExamsFromDatabase(semester: Semester, start: LocalDate): Flow> { - return examDb.loadAll( - diaryId = semester.diaryId, - studentId = semester.studentId, - from = start.startExamsDay, - end = start.endExamsDay - ) - } + fun getExamsFromDatabase( + semester: Semester, + start: LocalDate, + end: LocalDate + ): Flow> = examDb.loadAll( + diaryId = semester.diaryId, + studentId = semester.studentId, + from = start, + end = end, + ) suspend fun updateExam(exam: List) = examDb.updateAll(exam) } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/GradeRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/GradeRepository.kt index f5f895d82..1e2ea9354 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/GradeRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/GradeRepository.kt @@ -1,15 +1,22 @@ package io.github.wulkanowy.data.repositories import io.github.wulkanowy.data.db.dao.GradeDao +import io.github.wulkanowy.data.db.dao.GradeDescriptiveDao import io.github.wulkanowy.data.db.dao.GradeSummaryDao import io.github.wulkanowy.data.db.entities.Grade +import io.github.wulkanowy.data.db.entities.GradeDescriptive import io.github.wulkanowy.data.db.entities.GradeSummary import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.data.networkBoundResource import io.github.wulkanowy.sdk.Sdk -import io.github.wulkanowy.utils.* +import io.github.wulkanowy.utils.AutoRefreshHelper +import io.github.wulkanowy.utils.getRefreshKey +import io.github.wulkanowy.utils.init +import io.github.wulkanowy.utils.switchSemester +import io.github.wulkanowy.utils.toLocalDate +import io.github.wulkanowy.utils.uniqueSubtract import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map @@ -22,14 +29,13 @@ import javax.inject.Singleton class GradeRepository @Inject constructor( private val gradeDb: GradeDao, private val gradeSummaryDb: GradeSummaryDao, + private val gradeDescriptiveDb: GradeDescriptiveDao, private val sdk: Sdk, private val refreshHelper: AutoRefreshHelper, ) { private val saveFetchResultMutex = Mutex() - private val cacheKey = "grade" - fun getGrades( student: Student, semester: Semester, @@ -41,30 +47,52 @@ class GradeRepository @Inject constructor( //When details is empty and summary is not, app will not use summary cache - edge case it.first.isEmpty() }, - shouldFetch = { (details, summaries) -> - val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, semester)) - details.isEmpty() || summaries.isEmpty() || forceRefresh || isExpired + shouldFetch = { (details, summaries, descriptive) -> + val isExpired = + refreshHelper.shouldBeRefreshed(getRefreshKey(GRADE_CACHE_KEY, semester)) + details.isEmpty() || (summaries.isEmpty() && descriptive.isEmpty()) || forceRefresh || isExpired }, query = { val detailsFlow = gradeDb.loadAll(semester.semesterId, semester.studentId) val summaryFlow = gradeSummaryDb.loadAll(semester.semesterId, semester.studentId) - detailsFlow.combine(summaryFlow) { details, summaries -> details to summaries } + val descriptiveFlow = + gradeDescriptiveDb.loadAll(semester.semesterId, semester.studentId) + + combine(detailsFlow, summaryFlow, descriptiveFlow) { details, summaries, descriptive -> + Triple(details, summaries, descriptive) + } }, fetch = { - val (details, summary) = sdk.init(student) - .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) + val (details, summary, descriptive) = sdk.init(student) + .switchSemester(semester) .getGrades(semester.semesterId) - details.mapToEntities(semester) to summary.mapToEntities(semester) + Triple( + details.mapToEntities(semester), + summary.mapToEntities(semester), + descriptive.mapToEntities(semester) + ) }, - saveFetchResult = { (oldDetails, oldSummary), (newDetails, newSummary) -> + saveFetchResult = { (oldDetails, oldSummary, oldDescriptive), (newDetails, newSummary, newDescriptive) -> refreshGradeDetails(student, oldDetails, newDetails, notify) refreshGradeSummaries(oldSummary, newSummary, notify) + refreshGradeDescriptions(oldDescriptive, newDescriptive, notify) - refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester)) + refreshHelper.updateLastRefreshTimestamp(getRefreshKey(GRADE_CACHE_KEY, semester)) } ) + private suspend fun refreshGradeDescriptions( + old: List, + new: List, + notify: Boolean + ) { + gradeDescriptiveDb.deleteAll(old uniqueSubtract new) + gradeDescriptiveDb.insertAll((new uniqueSubtract old).onEach { + if (notify) it.isNotified = false + }) + } + private suspend fun refreshGradeDetails( student: Student, oldGrades: List, @@ -132,6 +160,10 @@ class GradeRepository @Inject constructor( return gradeSummaryDb.loadAll(semester.semesterId, semester.studentId) } + fun getGradesDescriptiveFromDatabase(semester: Semester): Flow> { + return gradeDescriptiveDb.loadAll(semester.semesterId, semester.studentId) + } + suspend fun updateGrade(grade: Grade) { return gradeDb.updateAll(listOf(grade)) } @@ -143,4 +175,13 @@ class GradeRepository @Inject constructor( suspend fun updateGradesSummary(gradesSummary: List) { return gradeSummaryDb.updateAll(gradesSummary) } + + suspend fun updateGradesDescriptive(gradesDescriptive: List) { + return gradeDescriptiveDb.updateAll(gradesDescriptive) + } + + private companion object { + + private const val GRADE_CACHE_KEY = "grade" + } } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/GradeStatisticsRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/GradeStatisticsRepository.kt index 9fa06c497..23d7b8582 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/GradeStatisticsRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/GradeStatisticsRepository.kt @@ -16,6 +16,7 @@ import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.AutoRefreshHelper import io.github.wulkanowy.utils.getRefreshKey import io.github.wulkanowy.utils.init +import io.github.wulkanowy.utils.switchSemester import io.github.wulkanowy.utils.uniqueSubtract import kotlinx.coroutines.sync.Mutex import java.util.* @@ -56,7 +57,7 @@ class GradeStatisticsRepository @Inject constructor( query = { gradePartialStatisticsDb.loadAll(semester.semesterId, semester.studentId) }, fetch = { sdk.init(student) - .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) + .switchSemester(semester) .getGradesPartialStatistics(semester.semesterId) .mapToEntities(semester) }, @@ -101,7 +102,7 @@ class GradeStatisticsRepository @Inject constructor( query = { gradeSemesterStatisticsDb.loadAll(semester.semesterId, semester.studentId) }, fetch = { sdk.init(student) - .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) + .switchSemester(semester) .getGradesSemesterStatistics(semester.semesterId) .mapToEntities(semester) }, @@ -157,7 +158,7 @@ class GradeStatisticsRepository @Inject constructor( query = { gradePointsStatisticsDb.loadAll(semester.semesterId, semester.studentId) }, fetch = { sdk.init(student) - .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) + .switchSemester(semester) .getGradesPointsStatistics(semester.semesterId) .mapToEntities(semester) }, diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/HomeworkRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/HomeworkRepository.kt index f564824de..010cf8458 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/HomeworkRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/HomeworkRepository.kt @@ -7,7 +7,13 @@ import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.data.networkBoundResource import io.github.wulkanowy.sdk.Sdk -import io.github.wulkanowy.utils.* +import io.github.wulkanowy.utils.AutoRefreshHelper +import io.github.wulkanowy.utils.getRefreshKey +import io.github.wulkanowy.utils.init +import io.github.wulkanowy.utils.monday +import io.github.wulkanowy.utils.sunday +import io.github.wulkanowy.utils.switchSemester +import io.github.wulkanowy.utils.uniqueSubtract import kotlinx.coroutines.sync.Mutex import java.time.LocalDate import javax.inject.Inject @@ -50,7 +56,7 @@ class HomeworkRepository @Inject constructor( }, fetch = { sdk.init(student) - .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) + .switchSemester(semester) .getHomework(start.monday, end.sunday) .mapToEntities(semester) }, diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/LuckyNumberRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/LuckyNumberRepository.kt index 87e8410f1..4ff4517d0 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/LuckyNumberRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/LuckyNumberRepository.kt @@ -35,12 +35,15 @@ class LuckyNumberRepository @Inject constructor( fetch = { sdk.init(student).getLuckyNumber(student.schoolShortName)?.mapToEntity(student) }, - saveFetchResult = { old, new -> - if (new != old) { - old?.let { luckyNumberDb.deleteAll(listOfNotNull(it)) } - luckyNumberDb.insertAll(listOfNotNull((new?.apply { - if (notify) isNotified = false - }))) + saveFetchResult = { oldLuckyNumber, newLuckyNumber -> + newLuckyNumber ?: return@networkBoundResource + + if (newLuckyNumber != oldLuckyNumber) { + val updatedLuckNumberList = + listOf(newLuckyNumber.apply { if (notify) isNotified = false }) + + oldLuckyNumber?.let { luckyNumberDb.deleteAll(listOfNotNull(it)) } + luckyNumberDb.insertAll(updatedLuckNumberList) } } ) diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/MailboxRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/MailboxRepository.kt deleted file mode 100644 index c571937ad..000000000 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/MailboxRepository.kt +++ /dev/null @@ -1,85 +0,0 @@ -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 e7428762e..c8fccb23d 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 @@ -5,16 +5,25 @@ import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.data.Resource import io.github.wulkanowy.data.db.SharedPrefProvider +import io.github.wulkanowy.data.db.dao.MailboxDao import io.github.wulkanowy.data.db.dao.MessageAttachmentDao import io.github.wulkanowy.data.db.dao.MessagesDao -import io.github.wulkanowy.data.db.entities.* +import io.github.wulkanowy.data.db.entities.Mailbox +import io.github.wulkanowy.data.db.entities.Message +import io.github.wulkanowy.data.db.entities.MessageWithAttachment +import io.github.wulkanowy.data.db.entities.Recipient +import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.enums.MessageFolder import io.github.wulkanowy.data.enums.MessageFolder.RECEIVED import io.github.wulkanowy.data.enums.MessageFolder.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.onResourceError +import io.github.wulkanowy.data.onResourceSuccess import io.github.wulkanowy.data.pojos.MessageDraft +import io.github.wulkanowy.data.waitForResult +import io.github.wulkanowy.domain.messages.GetMailboxByStudentUseCase import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.pojo.Folder import io.github.wulkanowy.utils.AutoRefreshHelper @@ -24,7 +33,6 @@ 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 @@ -40,16 +48,18 @@ class MessageRepository @Inject constructor( private val refreshHelper: AutoRefreshHelper, private val sharedPrefProvider: SharedPrefProvider, private val json: Json, + private val mailboxDao: MailboxDao, + private val getMailboxByStudentUseCase: GetMailboxByStudentUseCase, ) { private val saveFetchResultMutex = Mutex() - private val cacheKey = "message" + private val messagesCacheKey = "message" + private val mailboxCacheKey = "mailboxes" - @Suppress("UNUSED_PARAMETER") fun getMessages( student: Student, - mailbox: Mailbox, + mailbox: Mailbox?, folder: MessageFolder, forceRefresh: Boolean, notify: Boolean = false, @@ -58,16 +68,20 @@ class MessageRepository @Inject constructor( isResultEmpty = { it.isEmpty() }, shouldFetch = { val isExpired = refreshHelper.shouldBeRefreshed( - key = getRefreshKey(cacheKey, student, folder) + key = getRefreshKey(messagesCacheKey, mailbox, folder) ) it.isEmpty() || forceRefresh || isExpired }, - query = { messagesDb.loadAll(mailbox.globalKey, folder.id) }, + query = { + if (mailbox == null) { + messagesDb.loadAll(folder.id, student.email) + } else messagesDb.loadAll(mailbox.globalKey, folder.id) + }, fetch = { sdk.init(student).getMessages( folder = Folder.valueOf(folder.name), - mailboxKey = mailbox.globalKey, - ).mapToEntities(mailbox) + mailboxKey = mailbox?.globalKey, + ).mapToEntities(student, mailbox, mailboxDao.loadAll(student.email)) }, saveFetchResult = { old, new -> messagesDb.deleteAll(old uniqueSubtract new) @@ -75,7 +89,9 @@ class MessageRepository @Inject constructor( it.isNotified = !notify }) - refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, student, folder)) + refreshHelper.updateLastRefreshTimestamp( + getRefreshKey(messagesCacheKey, mailbox, folder) + ) } ) @@ -88,18 +104,26 @@ class MessageRepository @Inject constructor( shouldFetch = { 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() + (it.message.unread && markAsRead) || it.message.content.isBlank() + }, + query = { + messagesDb.loadMessageWithAttachment(message.messageGlobalKey) }, - query = { messagesDb.loadMessageWithAttachment(message.messageGlobalKey) }, fetch = { - sdk.init(student).getMessageDetails(it!!.message.messageGlobalKey, markAsRead) + sdk.init(student).getMessageDetails( + messageKey = it!!.message.messageGlobalKey, + markAsRead = message.unread && markAsRead, + ) }, saveFetchResult = { old, new -> checkNotNull(old) { "Fetched message no longer exist!" } messagesDb.updateAll( listOf(old.message.apply { id = message.id - unread = !markAsRead + unread = when { + markAsRead -> false + else -> unread + } sender = new.sender recipients = new.recipients.singleOrNull() ?: "Wielu adresatów" content = content.ifBlank { new.content } @@ -109,12 +133,14 @@ class MessageRepository @Inject constructor( items = new.attachments.mapToEntities(message.messageGlobalKey), ) - Timber.d("Message ${message.messageId} with blank content: ${old.message.content.isBlank()}, marked as read") + Timber.d("Message ${message.messageId} with blank content: ${old.message.content.isBlank()}, marked as read: $markAsRead") } ) - fun getMessagesFromDatabase(mailbox: Mailbox): Flow> { - return messagesDb.loadAll(mailbox.globalKey, RECEIVED.id) + fun getMessagesFromDatabase(student: Student, mailbox: Mailbox?): Flow> { + return if (mailbox == null) { + messagesDb.loadAll(RECEIVED.id, student.email) + } else messagesDb.loadAll(mailbox.globalKey, RECEIVED.id) } suspend fun updateMessages(messages: List) { @@ -136,7 +162,7 @@ class MessageRepository @Inject constructor( ) } - suspend fun deleteMessages(student: Student, mailbox: Mailbox, messages: List) { + suspend fun deleteMessages(student: Student, mailbox: Mailbox?, messages: List) { val firstMessage = messages.first() sdk.init(student).deleteMessages( messages = messages.map { it.messageGlobalKey }, @@ -165,10 +191,44 @@ class MessageRepository @Inject constructor( ).first() } - suspend fun deleteMessage(student: Student, mailbox: Mailbox, message: Message) { + suspend fun deleteMessage(student: Student, mailbox: Mailbox?, message: Message) { deleteMessages(student, mailbox, listOf(message)) } + suspend fun getMailboxes(student: Student, forceRefresh: Boolean) = networkBoundResource( + mutex = saveFetchResultMutex, + isResultEmpty = { it.isEmpty() }, + shouldFetch = { + val isExpired = refreshHelper.shouldBeRefreshed( + key = getRefreshKey(mailboxCacheKey, student), + ) + it.isEmpty() || isExpired || forceRefresh + }, + query = { mailboxDao.loadAll(student.email, student.symbol, student.schoolSymbol) }, + fetch = { + sdk.init(student).getMailboxes().mapToEntities(student) + }, + saveFetchResult = { old, new -> + mailboxDao.deleteAll(old uniqueSubtract new) + mailboxDao.insertAll(new uniqueSubtract old) + + refreshHelper.updateLastRefreshTimestamp(getRefreshKey(mailboxCacheKey, student)) + } + ) + + suspend fun getMailboxByStudent(student: Student): Mailbox? { + val mailbox = getMailboxByStudentUseCase(student) + + return if (mailbox == null) { + getMailboxes(student, forceRefresh = true) + .onResourceError { throw it } + .onResourceSuccess { Timber.i("Found ${it.size} new mailboxes") } + .waitForResult() + + getMailboxByStudentUseCase(student) + } else mailbox + } + var draftMessage: MessageDraft? get() = sharedPrefProvider.getString(context.getString(R.string.pref_key_message_draft)) ?.let { json.decodeFromString(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 07c6959e3..412f9e7f0 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/MobileDeviceRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/MobileDeviceRepository.kt @@ -12,6 +12,7 @@ import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.AutoRefreshHelper import io.github.wulkanowy.utils.getRefreshKey import io.github.wulkanowy.utils.init +import io.github.wulkanowy.utils.switchSemester import io.github.wulkanowy.utils.uniqueSubtract import kotlinx.coroutines.sync.Mutex import javax.inject.Inject @@ -42,7 +43,7 @@ class MobileDeviceRepository @Inject constructor( query = { mobileDb.loadAll(student.userLoginId) }, fetch = { sdk.init(student) - .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) + .switchSemester(semester) .getRegisteredDevices() .mapToEntities(student) }, @@ -56,7 +57,7 @@ class MobileDeviceRepository @Inject constructor( suspend fun unregisterDevice(student: Student, semester: Semester, device: MobileDevice) { sdk.init(student) - .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) + .switchSemester(semester) .unregisterDevice(device.deviceId) mobileDb.deleteAll(listOf(device)) @@ -64,7 +65,7 @@ class MobileDeviceRepository @Inject constructor( suspend fun getToken(student: Student, semester: Semester): MobileDeviceToken { return sdk.init(student) - .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) + .switchSemester(semester) .getToken() .mapToMobileDeviceToken() } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/NoteRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/NoteRepository.kt index e5d7bc5cb..eeb1d53ef 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 @@ -41,8 +41,8 @@ class NoteRepository @Inject constructor( query = { noteDb.loadAll(student.studentId) }, fetch = { sdk.init(student) - .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) - .getNotes(semester.semesterId) + .switchSemester(semester) + .getNotes() .mapToEntities(semester) }, saveFetchResult = { old, new -> 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 486538e0c..64e60a60b 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 @@ -2,25 +2,31 @@ package io.github.wulkanowy.data.repositories import android.content.Context import android.content.SharedPreferences +import androidx.annotation.StringRes import androidx.core.content.edit import com.fredporciuncula.flow.preferences.FlowSharedPreferences import com.fredporciuncula.flow.preferences.Preference +import com.fredporciuncula.flow.preferences.Serializer import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R -import io.github.wulkanowy.data.enums.* +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.TimetableGapsMode +import io.github.wulkanowy.data.enums.TimetableMode import io.github.wulkanowy.ui.modules.dashboard.DashboardItem import io.github.wulkanowy.ui.modules.grade.GradeAverageMode -import kotlinx.coroutines.ExperimentalCoroutinesApi +import io.github.wulkanowy.ui.modules.settings.appearance.menuorder.AppMenuItem 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.Instant +import java.util.UUID import javax.inject.Inject import javax.inject.Singleton -@OptIn(ExperimentalCoroutinesApi::class) @Singleton class PreferencesRepository @Inject constructor( @ApplicationContext val context: Context, @@ -29,29 +35,35 @@ class PreferencesRepository @Inject constructor( private val json: Json, ) { - val startMenuIndex: Int - get() = getString(R.string.pref_key_start_menu, R.string.pref_default_startup).toInt() - val isShowPresent: Boolean get() = getBoolean( R.string.pref_key_attendance_present, R.bool.pref_default_attendance_present ) - val gradeAverageMode: GradeAverageMode - get() = GradeAverageMode.getByValue( - getString( - R.string.pref_key_grade_average_mode, - R.string.pref_default_grade_average_mode - ) + private val gradeAverageModePref: Preference + get() = getObjectFlow( + R.string.pref_key_grade_average_mode, + R.string.pref_default_grade_average_mode, + object : Serializer { + override fun serialize(value: GradeAverageMode) = value.value + override fun deserialize(serialized: String) = + GradeAverageMode.getByValue(serialized) + }, ) - val gradeAverageForceCalc: Boolean - get() = getBoolean( - R.string.pref_key_grade_average_force_calc, - R.bool.pref_default_grade_average_force_calc + val gradeAverageModeFlow: Flow + get() = gradeAverageModePref.asFlow() + + private val gradeAverageForceCalcPref: Preference + get() = flowSharedPref.getBoolean( + context.getString(R.string.pref_key_grade_average_force_calc), + context.resources.getBoolean(R.bool.pref_default_grade_average_force_calc) ) + val gradeAverageForceCalcFlow: Flow + get() = gradeAverageForceCalcPref.asFlow() + val gradeExpandMode: GradeExpandMode get() = GradeExpandMode.getByValue( getString( @@ -141,12 +153,24 @@ class PreferencesRepository @Inject constructor( R.string.pref_default_grade_modifier_plus ).toDouble() + val gradePlusModifierFlow: Flow + get() = getStringFlow( + R.string.pref_key_grade_modifier_plus, + R.string.pref_default_grade_modifier_plus + ).asFlow().map { it.toDouble() } + val gradeMinusModifier: Double get() = getString( R.string.pref_key_grade_modifier_minus, R.string.pref_default_grade_modifier_minus ).toDouble() + val gradeMinusModifierFlow: Flow + get() = getStringFlow( + R.string.pref_key_grade_modifier_minus, + R.string.pref_default_grade_modifier_minus + ).asFlow().map { it.toDouble() } + val fillMessageContent: Boolean get() = getBoolean( R.string.pref_key_fill_message_content, @@ -175,30 +199,25 @@ class PreferencesRepository @Inject constructor( ) ) - val showTimetableTimers: Boolean - get() = getBoolean( - R.string.pref_key_timetable_show_timers, - R.bool.pref_default_timetable_show_timers + val showTimetableGaps: TimetableGapsMode + get() = TimetableGapsMode.getByValue( + getString( + R.string.pref_key_timetable_show_gaps, + R.string.pref_default_timetable_show_gaps + ) ) - var isHomeworkFullscreen: Boolean - get() = getBoolean( - R.string.pref_key_homework_fullscreen, - R.bool.pref_default_homework_fullscreen - ) - set(value) = sharedPref.edit().putBoolean("homework_fullscreen", value).apply() - val showSubjectsWithoutGrades: Boolean get() = getBoolean( R.string.pref_key_subjects_without_grades, R.bool.pref_default_subjects_without_grades ) - val isOptionalArithmeticAverage: Boolean - get() = getBoolean( - R.string.pref_key_optional_arithmetic_average, - R.bool.pref_default_optional_arithmetic_average - ) + val isOptionalArithmeticAverageFlow: Flow + get() = flowSharedPref.getBoolean( + context.getString(R.string.pref_key_optional_arithmetic_average), + context.resources.getBoolean(R.bool.pref_default_optional_arithmetic_average) + ).asFlow() var lasSyncDate: Instant? get() = getLong(R.string.pref_key_last_sync_date, R.string.pref_default_last_sync_date) @@ -289,19 +308,6 @@ class PreferencesRepository @Inject constructor( 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) @@ -316,11 +322,56 @@ class PreferencesRepository @Inject constructor( putBoolean(context.getString(R.string.pref_key_ads_enabled), value) } + var appMenuItemOrder: List + get() { + val value = sharedPref.getString(PREF_KEY_APP_MENU_ITEM_ORDER, null) + ?: return AppMenuItem.defaultAppMenuItemList + + return json.decodeFromString(value) + } + set(value) = sharedPref.edit { + putString( + PREF_KEY_APP_MENU_ITEM_ORDER, + json.encodeToString(value) + ) + } + + var isIncognitoMode: Boolean + get() = getBoolean(R.string.pref_key_incognito_moge, R.bool.pref_default_incognito_mode) + set(value) = sharedPref.edit { + putBoolean(context.getString(R.string.pref_key_incognito_moge), value) + } + + var installationId: String + get() = sharedPref.getString(PREF_KEY_INSTALLATION_ID, null).orEmpty() + private set(value) = sharedPref.edit { putString(PREF_KEY_INSTALLATION_ID, value) } + + init { + if (installationId.isEmpty()) { + installationId = UUID.randomUUID().toString() + } + } + private fun getLong(id: Int, default: Int) = getLong(context.getString(id), default) private fun getLong(id: String, default: Int) = sharedPref.getLong(id, context.resources.getString(default).toLong()) + private fun getStringFlow(id: Int, default: Int) = + flowSharedPref.getString(context.getString(id), context.getString(default)) + + private fun getObjectFlow( + @StringRes id: Int, + @StringRes default: Int, + serializer: Serializer + ): Preference = flowSharedPref.getObject( + key = context.getString(id), + serializer = serializer, + defaultValue = serializer.deserialize( + flowSharedPref.getString(context.getString(default)).get() + ) + ) + private fun getString(id: Int, default: Int) = getString(context.getString(id), default) private fun getString(id: String, default: Int) = @@ -331,23 +382,14 @@ class PreferencesRepository @Inject constructor( private fun getBoolean(id: String, default: Int) = sharedPref.getBoolean(id, context.resources.getBoolean(default)) - private fun getBoolean(id: Int, default: Boolean) = - sharedPref.getBoolean(context.getString(id), default) - private companion object { - + private const val PREF_KEY_APP_MENU_ITEM_ORDER = "app_menu_item_order" + private const val PREF_KEY_INSTALLATION_ID = "installation_id" private const val PREF_KEY_DASHBOARD_ITEMS_POSITION = "dashboard_items_position" - private const val PREF_KEY_IN_APP_REVIEW_COUNT = "in_app_review_count" - private const val PREF_KEY_IN_APP_REVIEW_DATE = "in_app_review_date" - 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 e80f028e1..79984ce6d 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 @@ -33,9 +33,11 @@ class RecipientRepository @Inject constructor( suspend fun getRecipients( student: Student, - mailbox: Mailbox, - type: MailboxType + mailbox: Mailbox?, + type: MailboxType, ): List { + mailbox ?: return emptyList() + val cached = recipientDb.loadAll(type, mailbox.globalKey) val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, student)) @@ -47,11 +49,15 @@ class RecipientRepository @Inject constructor( suspend fun getMessageSender( student: Student, - mailbox: Mailbox, - message: Message - ): List = sdk.init(student) - .getMessageReplayDetails(message.messageGlobalKey) - .sender - .let(::listOf) - .mapToEntities(mailbox.globalKey) + mailbox: Mailbox?, + message: Message, + ): List { + mailbox ?: return emptyList() + + return sdk.init(student) + .getMessageReplayDetails(message.messageGlobalKey) + .sender + .let(::listOf) + .mapToEntities(mailbox.globalKey) + } } 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 7972ed084..f757ef047 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolRepository.kt @@ -9,6 +9,7 @@ import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.AutoRefreshHelper import io.github.wulkanowy.utils.getRefreshKey import io.github.wulkanowy.utils.init +import io.github.wulkanowy.utils.switchSemester import kotlinx.coroutines.sync.Mutex import javax.inject.Inject import javax.inject.Singleton @@ -40,7 +41,7 @@ class SchoolRepository @Inject constructor( query = { schoolDb.load(semester.studentId, semester.classId) }, fetch = { sdk.init(student) - .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) + .switchSemester(semester) .getSchool() .mapToEntity(semester) }, diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolsRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolsRepository.kt new file mode 100644 index 000000000..216a8c112 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolsRepository.kt @@ -0,0 +1,65 @@ +package io.github.wulkanowy.data.repositories + +import io.github.wulkanowy.data.api.SchoolsService +import io.github.wulkanowy.data.db.entities.Semester +import io.github.wulkanowy.data.db.entities.Student +import io.github.wulkanowy.data.db.entities.StudentWithSemesters +import io.github.wulkanowy.data.pojos.IntegrityRequest +import io.github.wulkanowy.data.pojos.LoginEvent +import io.github.wulkanowy.sdk.Sdk +import io.github.wulkanowy.ui.modules.login.LoginData +import io.github.wulkanowy.utils.IntegrityHelper +import io.github.wulkanowy.utils.getCurrentOrLast +import io.github.wulkanowy.utils.init +import io.github.wulkanowy.utils.switchSemester +import kotlinx.coroutines.withTimeout +import timber.log.Timber +import java.util.UUID +import javax.inject.Inject +import javax.inject.Singleton +import kotlin.time.Duration.Companion.seconds + +@Singleton +class SchoolsRepository @Inject constructor( + private val integrityHelper: IntegrityHelper, + private val schoolsService: SchoolsService, + private val sdk: Sdk, +) { + + suspend fun logSchoolLogin(loginData: LoginData, students: List) { + students.forEach { + runCatching { + withTimeout(10.seconds) { + logLogin(loginData, it.student, it.semesters.getCurrentOrLast()) + } + } + .onFailure { Timber.e(it) } + } + } + + private suspend fun logLogin(loginData: LoginData, student: Student, semester: Semester) { + val requestId = UUID.randomUUID().toString() + val token = integrityHelper.getIntegrityToken(requestId) ?: return + + val schoolInfo = sdk + .init(student.copy(password = loginData.password)) + .switchSemester(semester) + .getSchool() + + schoolsService.logLoginEvent( + IntegrityRequest( + tokenString = token, + data = LoginEvent( + uuid = requestId, + schoolAddress = schoolInfo.address, + schoolName = schoolInfo.name, + schoolShort = student.schoolShortName, + scraperBaseUrl = student.scrapperBaseUrl, + loginType = student.loginType, + symbol = student.symbol, + schoolId = student.schoolSymbol, + ) + ) + ) + } +} 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 96f019223..dd44df70f 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 @@ -40,8 +40,8 @@ class SemesterRepository @Inject constructor( val isNoSemesters = semesters.isEmpty() val isRefreshOnModeChangeRequired = when { - Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API -> { - semesters.firstOrNull { it.isCurrent }?.let { + Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE -> { + semesters.firstOrNull { it.isCurrent() }?.let { 0 == it.diaryId && 0 == it.kindergartenDiaryId } == true } @@ -49,7 +49,7 @@ class SemesterRepository @Inject constructor( } val isRefreshOnNoCurrentAppropriate = - refreshOnNoCurrent && !semesters.any { semester -> semester.isCurrent } + refreshOnNoCurrent && !semesters.any { semester -> semester.isCurrent() } return forceRefresh || isNoSemesters || isRefreshOnModeChangeRequired || isRefreshOnNoCurrentAppropriate } 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 efc82a772..d6cd25c82 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/StudentInfoRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/StudentInfoRepository.kt @@ -7,6 +7,7 @@ import io.github.wulkanowy.data.mappers.mapToEntity import io.github.wulkanowy.data.networkBoundResource import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.init +import io.github.wulkanowy.utils.switchSemester import kotlinx.coroutines.sync.Mutex import javax.inject.Inject import javax.inject.Singleton @@ -30,7 +31,7 @@ class StudentInfoRepository @Inject constructor( query = { studentInfoDao.loadStudentInfo(student.studentId) }, fetch = { sdk.init(student) - .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) + .switchSemester(semester) .getStudentInfo().mapToEntity(semester) }, saveFetchResult = { old, new -> diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt index f006b7d28..e063840cb 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt @@ -1,83 +1,90 @@ package io.github.wulkanowy.data.repositories -import android.content.Context import androidx.room.withTransaction -import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.data.db.AppDatabase import io.github.wulkanowy.data.db.dao.SemesterDao import io.github.wulkanowy.data.db.dao.StudentDao +import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student +import io.github.wulkanowy.data.db.entities.StudentName import io.github.wulkanowy.data.db.entities.StudentNickAndAvatar import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.data.exceptions.NoCurrentStudentException -import io.github.wulkanowy.data.mappers.mapToEntities +import io.github.wulkanowy.data.mappers.mapToPojo +import io.github.wulkanowy.data.pojos.RegisterUser import io.github.wulkanowy.sdk.Sdk -import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.DispatchersProvider -import io.github.wulkanowy.utils.security.decrypt -import io.github.wulkanowy.utils.security.encrypt +import io.github.wulkanowy.utils.init +import io.github.wulkanowy.utils.security.Scrambler +import io.github.wulkanowy.utils.switchSemester import kotlinx.coroutines.withContext import javax.inject.Inject import javax.inject.Singleton @Singleton class StudentRepository @Inject constructor( - @ApplicationContext private val context: Context, private val dispatchers: DispatchersProvider, private val studentDb: StudentDao, private val semesterDb: SemesterDao, private val sdk: Sdk, - private val appInfo: AppInfo, - private val appDatabase: AppDatabase + private val appDatabase: AppDatabase, + private val scrambler: Scrambler, ) { - suspend fun isStudentSaved() = getSavedStudents(false).isNotEmpty() - suspend fun isCurrentStudentSet() = studentDb.loadCurrent()?.isCurrent ?: false suspend fun getStudentsApi( pin: String, symbol: String, token: String - ): List = - sdk.getStudentsFromMobileApi(token, pin, symbol, "") - .mapToEntities(colors = appInfo.defaultColorsForAvatar) + ): RegisterUser = sdk + .getStudentsFromHebe(token, pin, symbol, "") + .mapToPojo(null) - suspend fun getStudentsScrapper( + suspend fun getUserSubjectsFromScrapper( email: String, password: String, scrapperBaseUrl: String, + domainSuffix: String, symbol: String - ): List = - sdk.getStudentsFromScrapper(email, password, scrapperBaseUrl, symbol) - .mapToEntities(password, appInfo.defaultColorsForAvatar) + ): RegisterUser = sdk + .getUserSubjectsFromScrapper(email, password, scrapperBaseUrl, domainSuffix, symbol) + .mapToPojo(password) suspend fun getStudentsHybrid( email: String, password: String, scrapperBaseUrl: String, symbol: String - ): List = - sdk.getStudentsHybrid(email, password, scrapperBaseUrl, "", symbol) - .mapToEntities(password, appInfo.defaultColorsForAvatar) + ): RegisterUser = sdk + .getStudentsHybrid(email, password, scrapperBaseUrl, "", symbol) + .mapToPojo(password) - suspend fun getSavedStudents(decryptPass: Boolean = true) = - studentDb.loadStudentsWithSemesters() - .map { - it.apply { - if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API) { + suspend fun getSavedStudents(decryptPass: Boolean = true): List { + return studentDb.loadStudentsWithSemesters().map { (student, semesters) -> + StudentWithSemesters( + student = student.apply { + if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE) { student.password = withContext(dispatchers.io) { - decrypt(student.password) + scrambler.decrypt(student.password) } } - } - } + }, + semesters = semesters, + ) + } + } - suspend fun getSavedStudentById(id: Long, decryptPass: Boolean = true) = - studentDb.loadStudentWithSemestersById(id)?.apply { - if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API) { + suspend fun getSavedStudentById(id: Long, decryptPass: Boolean = true): StudentWithSemesters? = + studentDb.loadStudentWithSemestersById(id).let { res -> + StudentWithSemesters( + student = res.keys.firstOrNull() ?: return null, + semesters = res.values.first(), + ) + }.apply { + if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE) { student.password = withContext(dispatchers.io) { - decrypt(student.password) + scrambler.decrypt(student.password) } } } @@ -85,9 +92,9 @@ class StudentRepository @Inject constructor( suspend fun getStudentById(id: Long, decryptPass: Boolean = true): Student { val student = studentDb.loadById(id) ?: throw NoCurrentStudentException() - if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API) { + if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE) { student.password = withContext(dispatchers.io) { - decrypt(student.password) + scrambler.decrypt(student.password) } } return student @@ -96,9 +103,9 @@ class StudentRepository @Inject constructor( suspend fun getCurrentStudent(decryptPass: Boolean = true): Student { val student = studentDb.loadCurrent() ?: throw NoCurrentStudentException() - if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API) { + if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE) { student.password = withContext(dispatchers.io) { - decrypt(student.password) + scrambler.decrypt(student.password) } } return student @@ -109,9 +116,9 @@ class StudentRepository @Inject constructor( val students = studentsWithSemesters.map { it.student } .map { it.apply { - if (Sdk.Mode.valueOf(it.loginMode) != Sdk.Mode.API) { + if (Sdk.Mode.valueOf(it.loginMode) != Sdk.Mode.HEBE) { password = withContext(dispatchers.io) { - encrypt(password, context) + scrambler.encrypt(password) } } } @@ -140,4 +147,32 @@ class StudentRepository @Inject constructor( suspend fun isOneUniqueStudent() = getSavedStudents(false) .distinctBy { it.student.studentName }.size == 1 + + suspend fun authorizePermission(student: Student, semester: Semester, pesel: String) = + sdk.init(student) + .switchSemester(semester) + .authorizePermission(pesel) + + suspend fun refreshStudentName(student: Student, semester: Semester) { + val newCurrentApiStudent = sdk.init(student) + .switchSemester(semester) + .getCurrentStudent() ?: return + + val studentName = StudentName( + studentName = "${newCurrentApiStudent.studentName} ${newCurrentApiStudent.studentSurname}" + ).apply { id = student.id } + + studentDb.update(studentName) + } + + suspend fun deleteStudentsAssociatedWithAccount(student: Student) { + studentDb.deleteByEmailAndUserName(student.email, student.userName) + } + + suspend fun clearAll() { + withContext(dispatchers.io) { + scrambler.clearKeyPair() + appDatabase.clearAllTables() + } + } } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/SubjectRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/SubjectRepository.kt index 3926122b3..98cb181af 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/SubjectRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/SubjectRepository.kt @@ -9,6 +9,7 @@ import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.AutoRefreshHelper import io.github.wulkanowy.utils.getRefreshKey import io.github.wulkanowy.utils.init +import io.github.wulkanowy.utils.switchSemester import io.github.wulkanowy.utils.uniqueSubtract import kotlinx.coroutines.sync.Mutex import javax.inject.Inject @@ -39,8 +40,9 @@ class SubjectRepository @Inject constructor( query = { subjectDao.loadAll(semester.diaryId, semester.studentId) }, fetch = { sdk.init(student) - .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) - .getSubjects().mapToEntities(semester) + .switchSemester(semester) + .getSubjects() + .mapToEntities(semester) }, saveFetchResult = { old, new -> subjectDao.deleteAll(old uniqueSubtract new) diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/TeacherRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/TeacherRepository.kt index acd71e1f4..42698f922 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/TeacherRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/TeacherRepository.kt @@ -9,6 +9,7 @@ import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.AutoRefreshHelper import io.github.wulkanowy.utils.getRefreshKey import io.github.wulkanowy.utils.init +import io.github.wulkanowy.utils.switchSemester import io.github.wulkanowy.utils.uniqueSubtract import kotlinx.coroutines.sync.Mutex import javax.inject.Inject @@ -39,8 +40,8 @@ class TeacherRepository @Inject constructor( query = { teacherDb.loadAll(semester.studentId, semester.classId) }, fetch = { sdk.init(student) - .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) - .getTeachers(semester.semesterId) + .switchSemester(semester) + .getTeachers() .mapToEntities(semester) }, saveFetchResult = { old, new -> 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 3145c2a23..9305d3b31 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/TimetableRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/TimetableRepository.kt @@ -13,6 +13,7 @@ import io.github.wulkanowy.utils.* import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.sync.Mutex +import java.time.Instant import java.time.LocalDate import javax.inject.Inject import javax.inject.Singleton @@ -64,8 +65,8 @@ class TimetableRepository @Inject constructor( query = { getFullTimetableFromDatabase(student, semester, start, end) }, fetch = { val timetableFull = sdk.init(student) - .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) - .getTimetableFull(start.monday, end.sunday) + .switchSemester(semester) + .getTimetable(start.monday, end.sunday) timetableFull.mapToEntities(semester) }, @@ -164,6 +165,11 @@ class TimetableRepository @Inject constructor( timetableHeaderDb.insertAll(new uniqueSubtract old) } + fun getLastRefreshTimestamp(semester: Semester, start: LocalDate, end: LocalDate): Instant { + val refreshKey = getRefreshKey(cacheKey, semester, start, end) + return refreshHelper.getLastRefreshTimestamp(refreshKey) + } + suspend fun saveAdditionalList(additionalList: List) = timetableAdditionalDb.insertAll(additionalList) diff --git a/app/src/main/java/io/github/wulkanowy/domain/adminmessage/GetAppropriateAdminMessageUseCase.kt b/app/src/main/java/io/github/wulkanowy/domain/adminmessage/GetAppropriateAdminMessageUseCase.kt new file mode 100644 index 000000000..b55bf899d --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/domain/adminmessage/GetAppropriateAdminMessageUseCase.kt @@ -0,0 +1,64 @@ +package io.github.wulkanowy.domain.adminmessage + +import io.github.wulkanowy.data.Resource +import io.github.wulkanowy.data.db.entities.AdminMessage +import io.github.wulkanowy.data.db.entities.Student +import io.github.wulkanowy.data.enums.MessageType +import io.github.wulkanowy.data.mapResourceData +import io.github.wulkanowy.data.repositories.AdminMessageRepository +import io.github.wulkanowy.data.repositories.PreferencesRepository +import io.github.wulkanowy.utils.AppInfo +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject + +class GetAppropriateAdminMessageUseCase @Inject constructor( + private val adminMessageRepository: AdminMessageRepository, + private val preferencesRepository: PreferencesRepository, + private val appInfo: AppInfo +) { + + operator fun invoke(student: Student, type: MessageType): Flow> { + return invoke(student.scrapperBaseUrl, type) + } + + operator fun invoke(scrapperBaseUrl: String, type: MessageType): Flow> { + return adminMessageRepository.getAdminMessages().mapResourceData { adminMessages -> + adminMessages + .asSequence() + .filter { it.isNotDismissed() } + .filter { it.isVersionMatch() } + .filter { it.isRegisterHostMatch(scrapperBaseUrl) } + .filter { it.isFlavorMatch() } + .filter { it.isTypeMatch(type) } + .maxByOrNull { it.id } + } + } + + private fun AdminMessage.isNotDismissed(): Boolean { + return id !in preferencesRepository.dismissedAdminMessageIds + } + + private fun AdminMessage.isRegisterHostMatch(scrapperBaseUrl: String): Boolean { + return targetRegisterHost?.let { + scrapperBaseUrl.contains(it, true) + } ?: true + } + + private fun AdminMessage.isFlavorMatch(): Boolean { + return targetFlavor?.equals(appInfo.buildFlavor, true) ?: true + } + + private fun AdminMessage.isVersionMatch(): Boolean { + val isCorrectMaxVersion = versionMax?.let { it >= appInfo.versionCode } ?: true + val isCorrectMinVersion = versionMin?.let { it <= appInfo.versionCode } ?: true + + return isCorrectMaxVersion && isCorrectMinVersion + } + + private fun AdminMessage.isTypeMatch(messageType: MessageType): Boolean { + if (messageType in types) return true + if (MessageType.GENERAL_MESSAGE in types) return true + + return false + } +} diff --git a/app/src/main/java/io/github/wulkanowy/domain/messages/GetMailboxByStudentUseCase.kt b/app/src/main/java/io/github/wulkanowy/domain/messages/GetMailboxByStudentUseCase.kt new file mode 100644 index 000000000..669514aae --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/domain/messages/GetMailboxByStudentUseCase.kt @@ -0,0 +1,65 @@ +package io.github.wulkanowy.domain.messages + +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 javax.inject.Inject + +class GetMailboxByStudentUseCase @Inject constructor( + private val mailboxDao: MailboxDao, +) { + + suspend operator fun invoke(student: Student): Mailbox? { + return mailboxDao.loadAll(student.email) + .filterByStudent(student) + } + + private fun List.filterByStudent(student: Student): Mailbox? { + val normalizedStudentName = student.studentName.normalizeStudentName() + + return singleOrNull { + it.studentName.normalizeStudentName() == normalizedStudentName + } ?: singleOrNull { + it.studentName.normalizeStudentName() == normalizedStudentName + && it.schoolNameShort == student.schoolShortName + } ?: singleOrNull { + it.studentName.getFirstAndLastPart() == normalizedStudentName.getFirstAndLastPart() + } ?: singleOrNull { + it.studentName.getReversedName() == normalizedStudentName + } ?: 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.getReversedName(): String { + val parts = normalizeStudentName().split(" ") + + return parts + .asReversed() + .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/domain/timetable/IsStudentHasLessonsOnWeekendUseCase.kt b/app/src/main/java/io/github/wulkanowy/domain/timetable/IsStudentHasLessonsOnWeekendUseCase.kt new file mode 100644 index 000000000..efe928e2b --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/domain/timetable/IsStudentHasLessonsOnWeekendUseCase.kt @@ -0,0 +1,33 @@ +package io.github.wulkanowy.domain.timetable + +import io.github.wulkanowy.data.dataOrNull +import io.github.wulkanowy.data.db.entities.Semester +import io.github.wulkanowy.data.db.entities.Student +import io.github.wulkanowy.data.repositories.TimetableRepository +import io.github.wulkanowy.data.toFirstResult +import io.github.wulkanowy.utils.monday +import io.github.wulkanowy.utils.sunday +import java.time.LocalDate +import javax.inject.Inject + +class IsStudentHasLessonsOnWeekendUseCase @Inject constructor( + private val timetableRepository: TimetableRepository, + private val isWeekendHasLessonsUseCase: IsWeekendHasLessonsUseCase, +) { + + suspend operator fun invoke( + student: Student, + semester: Semester, + currentDate: LocalDate = LocalDate.now(), + ): Boolean { + val lessons = timetableRepository.getTimetable( + student = student, + semester = semester, + start = currentDate.monday, + end = currentDate.sunday, + forceRefresh = false, + timetableType = TimetableRepository.TimetableType.NORMAL + ).toFirstResult().dataOrNull?.lessons.orEmpty() + return isWeekendHasLessonsUseCase(lessons) + } +} diff --git a/app/src/main/java/io/github/wulkanowy/domain/timetable/IsWeekendHasLessonsUseCase.kt b/app/src/main/java/io/github/wulkanowy/domain/timetable/IsWeekendHasLessonsUseCase.kt new file mode 100644 index 000000000..908c9df78 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/domain/timetable/IsWeekendHasLessonsUseCase.kt @@ -0,0 +1,17 @@ +package io.github.wulkanowy.domain.timetable + +import io.github.wulkanowy.data.db.entities.Timetable +import java.time.DayOfWeek +import javax.inject.Inject + +class IsWeekendHasLessonsUseCase @Inject constructor() { + + operator fun invoke( + lessons: List, + ): Boolean = lessons.any { + it.date.dayOfWeek in listOf( + DayOfWeek.SATURDAY, + DayOfWeek.SUNDAY, + ) + } +} diff --git a/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationSchedulerHelper.kt b/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationSchedulerHelper.kt index 42078d03f..aae7882f1 100644 --- a/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationSchedulerHelper.kt +++ b/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationSchedulerHelper.kt @@ -65,8 +65,6 @@ class TimetableNotificationSchedulerHelper @Inject constructor( range = lesson.start..lesson.end, requestCode = getRequestCode(lesson.start, studentId) ) - - Timber.d("TimetableNotification canceled: type 1 & 2, subject: ${lesson.subject}, start: ${lesson.start}, student: $studentId") } } } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/SyncManager.kt b/app/src/main/java/io/github/wulkanowy/services/sync/SyncManager.kt index c1bed4dd3..e0a136f98 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 @@ -4,18 +4,12 @@ import android.os.Build.VERSION.SDK_INT import android.os.Build.VERSION_CODES.O import androidx.core.app.NotificationManagerCompat import androidx.lifecycle.asFlow +import androidx.work.* import androidx.work.BackoffPolicy.EXPONENTIAL -import androidx.work.Constraints -import androidx.work.Data import androidx.work.ExistingPeriodicWorkPolicy.KEEP -import androidx.work.ExistingPeriodicWorkPolicy.REPLACE -import androidx.work.ExistingWorkPolicy +import androidx.work.ExistingPeriodicWorkPolicy.UPDATE import androidx.work.NetworkType.CONNECTED import androidx.work.NetworkType.UNMETERED -import androidx.work.OneTimeWorkRequestBuilder -import androidx.work.PeriodicWorkRequestBuilder -import androidx.work.WorkInfo -import androidx.work.WorkManager import io.github.wulkanowy.data.db.SharedPrefProvider import io.github.wulkanowy.data.db.SharedPrefProvider.Companion.APP_VERSION_CODE_KEY import io.github.wulkanowy.data.repositories.PreferencesRepository @@ -60,7 +54,7 @@ class SyncManager @Inject constructor( val serviceInterval = preferencesRepository.servicesInterval workManager.enqueueUniquePeriodicWork( - SyncWorker::class.java.simpleName, if (restart) REPLACE else KEEP, + SyncWorker::class.java.simpleName, if (restart) UPDATE else KEEP, PeriodicWorkRequestBuilder(serviceInterval, MINUTES) .setInitialDelay(10, MINUTES) .setBackoffCriteria(EXPONENTIAL, 30, MINUTES) 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 9b49ed178..7f90bbddc 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewGradeNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewGradeNotification.kt @@ -4,12 +4,12 @@ import android.content.Context import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Grade +import io.github.wulkanowy.data.db.entities.GradeDescriptive import io.github.wulkanowy.data.db.entities.GradeSummary import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.pojos.GroupNotificationData import io.github.wulkanowy.data.pojos.NotificationData import io.github.wulkanowy.ui.modules.Destination -import io.github.wulkanowy.ui.modules.splash.SplashActivity import io.github.wulkanowy.utils.getPlural import javax.inject.Inject @@ -88,4 +88,28 @@ class NewGradeNotification @Inject constructor( appNotificationManager.sendMultipleNotifications(groupNotificationData, student) } + + suspend fun notifyDescriptive(items: List, student: Student) { + val notificationDataList = items.map { + NotificationData( + title = context.getPlural(R.plurals.grade_new_items_descriptive, 1), + content = "${it.subject}: ${it.description}", + destination = Destination.Grade, + ) + } + + val groupNotificationData = GroupNotificationData( + notificationDataList = notificationDataList, + title = context.getPlural(R.plurals.grade_new_items_descriptive, items.size), + content = context.getPlural( + R.plurals.grade_notify_new_items_descriptive, + items.size, + items.size + ), + destination = Destination.Grade, + type = NotificationType.NEW_GRADE_DESCRIPTIVE + ) + + appNotificationManager.sendMultipleNotifications(groupNotificationData, student) + } } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewMessageNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewMessageNotification.kt index 3b7bcff05..45523d51e 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 @@ -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.getPlural import javax.inject.Inject diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NotificationType.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NotificationType.kt index 023ae2e4c..4e7f27351 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NotificationType.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NotificationType.kt @@ -37,6 +37,10 @@ enum class NotificationType( channel = NewGradesChannel.CHANNEL_ID, icon = R.drawable.ic_stat_grade, ), + NEW_GRADE_DESCRIPTIVE( + channel = NewGradesChannel.CHANNEL_ID, + icon = R.drawable.ic_stat_grade, + ), NEW_HOMEWORK( channel = NewHomeworkChannel.CHANNEL_ID, icon = R.drawable.ic_more_homework, diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/works/AttendanceWork.kt b/app/src/main/java/io/github/wulkanowy/services/sync/works/AttendanceWork.kt index 657f69638..4fc097492 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/works/AttendanceWork.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/works/AttendanceWork.kt @@ -16,17 +16,24 @@ class AttendanceWork @Inject constructor( ) : Work { override suspend fun doWork(student: Student, semester: Semester, notify: Boolean) { + val startDate = now().previousOrSameSchoolDay + val endDate = startDate.plusDays(7) + attendanceRepository.getAttendance( student = student, semester = semester, - start = now().previousOrSameSchoolDay, - end = now().previousOrSameSchoolDay, + start = startDate, + end = endDate, forceRefresh = true, notify = notify, ) .waitForResult() - attendanceRepository.getAttendanceFromDatabase(semester, now().minusDays(7), now()) + attendanceRepository.getAttendanceFromDatabase( + semester = semester, + start = startDate, + end = endDate, + ) .first() .filterNot { it.isNotified } .let { diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/works/ExamWork.kt b/app/src/main/java/io/github/wulkanowy/services/sync/works/ExamWork.kt index 7071bce20..4b0b1bdb3 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/works/ExamWork.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/works/ExamWork.kt @@ -5,6 +5,8 @@ import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.repositories.ExamRepository import io.github.wulkanowy.data.waitForResult import io.github.wulkanowy.services.sync.notifications.NewExamNotification +import io.github.wulkanowy.utils.endExamsDay +import io.github.wulkanowy.utils.startExamsDay import kotlinx.coroutines.flow.first import java.time.LocalDate.now import javax.inject.Inject @@ -15,16 +17,24 @@ class ExamWork @Inject constructor( ) : Work { override suspend fun doWork(student: Student, semester: Semester, notify: Boolean) { + val startDate = now().startExamsDay + val endDate = startDate.endExamsDay + examRepository.getExams( student = student, semester = semester, - start = now(), - end = now(), + start = startDate, + end = endDate, forceRefresh = true, notify = notify, ).waitForResult() - examRepository.getExamsFromDatabase(semester, now()).first() + examRepository.getExamsFromDatabase( + semester = semester, + start = startDate, + end = endDate, + ) + .first() .filter { !it.isNotified }.let { if (it.isNotEmpty()) newExamNotification.notify(it, student) diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/works/GradeWork.kt b/app/src/main/java/io/github/wulkanowy/services/sync/works/GradeWork.kt index ba21b8600..b62ad94b9 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/works/GradeWork.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/works/GradeWork.kt @@ -45,5 +45,15 @@ class GradeWork @Inject constructor( grade.isFinalGradeNotified = true }) } + + gradeRepository.getGradesDescriptiveFromDatabase(semester).first() + .filter { !it.isNotified } + .let { + if (it.isNotEmpty()) newGradeNotification.notifyDescriptive(it, student) + + gradeRepository.updateGradesDescriptive(it.onEach { grade -> + grade.isNotified = true + }) + } } } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/works/HomeworkWork.kt b/app/src/main/java/io/github/wulkanowy/services/sync/works/HomeworkWork.kt index 4cfe27d0d..ddff3af7c 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/works/HomeworkWork.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/works/HomeworkWork.kt @@ -5,7 +5,9 @@ import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.repositories.HomeworkRepository import io.github.wulkanowy.data.waitForResult import io.github.wulkanowy.services.sync.notifications.NewHomeworkNotification +import io.github.wulkanowy.utils.monday import io.github.wulkanowy.utils.nextOrSameSchoolDay +import io.github.wulkanowy.utils.sunday import kotlinx.coroutines.flow.first import java.time.LocalDate.now import javax.inject.Inject @@ -16,16 +18,24 @@ class HomeworkWork @Inject constructor( ) : Work { override suspend fun doWork(student: Student, semester: Semester, notify: Boolean) { + val startDate = now().nextOrSameSchoolDay.monday + val endDate = startDate.sunday + homeworkRepository.getHomework( student = student, semester = semester, - start = now().nextOrSameSchoolDay, - end = now().nextOrSameSchoolDay, + start = startDate, + end = endDate, forceRefresh = true, notify = notify, ).waitForResult() - homeworkRepository.getHomeworkFromDatabase(semester, now(), now().plusDays(7)).first() + homeworkRepository.getHomeworkFromDatabase( + semester = semester, + start = startDate, + end = endDate + ) + .first() .filter { !it.isNotified }.let { if (it.isNotEmpty()) newHomeworkNotification.notify(it, student) diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/works/MessageWork.kt b/app/src/main/java/io/github/wulkanowy/services/sync/works/MessageWork.kt index 180568267..c7824e61f 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,7 +3,6 @@ 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.waitForResult import io.github.wulkanowy.services.sync.notifications.NewMessageNotification @@ -12,12 +11,11 @@ import javax.inject.Inject class MessageWork @Inject constructor( private val messageRepository: MessageRepository, - private val mailboxRepository: MailboxRepository, private val newMessageNotification: NewMessageNotification, ) : Work { override suspend fun doWork(student: Student, semester: Semester, notify: Boolean) { - val mailbox = mailboxRepository.getMailbox(student) + val mailbox = messageRepository.getMailboxByStudent(student) messageRepository.getMessages( student = student, mailbox = mailbox, @@ -26,7 +24,7 @@ class MessageWork @Inject constructor( notify = notify ).waitForResult() - messageRepository.getMessagesFromDatabase(mailbox).first() + messageRepository.getMessagesFromDatabase(student, 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/RecipientWork.kt b/app/src/main/java/io/github/wulkanowy/services/sync/works/RecipientWork.kt index b1322ada3..90b20651d 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,22 +1,23 @@ package io.github.wulkanowy.services.sync.works +import io.github.wulkanowy.data.dataOrNull 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.MessageRepository import io.github.wulkanowy.data.repositories.RecipientRepository +import io.github.wulkanowy.data.toFirstResult import javax.inject.Inject class RecipientWork @Inject constructor( - private val mailboxRepository: MailboxRepository, + private val messageRepository: MessageRepository, private val recipientRepository: RecipientRepository ) : Work { override suspend fun doWork(student: Student, semester: Semester, notify: Boolean) { - mailboxRepository.refreshMailboxes(student) - - val mailbox = mailboxRepository.getMailbox(student) - - recipientRepository.refreshRecipients(student, mailbox, MailboxType.EMPLOYEE) + val mailboxes = messageRepository.getMailboxes(student, forceRefresh = true).toFirstResult() + mailboxes.dataOrNull?.forEach { + recipientRepository.refreshRecipients(student, it, MailboxType.EMPLOYEE) + } } } 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 29b1f13c7..ac9a8eb4c 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 @@ -16,17 +16,24 @@ class TimetableWork @Inject constructor( ) : Work { override suspend fun doWork(student: Student, semester: Semester, notify: Boolean) { + val startDate = now().nextOrSameSchoolDay + val endDate = startDate.plusDays(7) + timetableRepository.getTimetable( student = student, semester = semester, - start = now().nextOrSameSchoolDay, - end = now().nextOrSameSchoolDay, + start = startDate, + end = endDate, forceRefresh = true, notify = notify, ) .waitForResult() - timetableRepository.getTimetableFromDatabase(semester, now(), now().plusDays(7)) + timetableRepository.getTimetableFromDatabase( + semester = semester, + from = startDate, + end = endDate, + ) .first() .filterNot { it.isNotified } .let { diff --git a/app/src/main/java/io/github/wulkanowy/services/widgets/TimetableWidgetService.kt b/app/src/main/java/io/github/wulkanowy/services/widgets/TimetableWidgetService.kt index 45cd2b04e..ffdb07ecd 100644 --- a/app/src/main/java/io/github/wulkanowy/services/widgets/TimetableWidgetService.kt +++ b/app/src/main/java/io/github/wulkanowy/services/widgets/TimetableWidgetService.kt @@ -25,13 +25,21 @@ class TimetableWidgetService : RemoteViewsService() { lateinit var semesterRepo: SemesterRepository @Inject - lateinit var prefRepository: PreferencesRepository + lateinit var sharedPref: SharedPrefProvider @Inject - lateinit var sharedPref: SharedPrefProvider + lateinit var prefRepository: PreferencesRepository override fun onGetViewFactory(intent: Intent?): RemoteViewsFactory { Timber.d("TimetableWidgetFactory created") - return TimetableWidgetFactory(timetableRepo, studentRepo, semesterRepo, prefRepository, sharedPref, applicationContext, intent) + return TimetableWidgetFactory( + timetableRepository = timetableRepo, + studentRepository = studentRepo, + semesterRepository = semesterRepo, + sharedPref = sharedPref, + prefRepository = prefRepository, + context = applicationContext, + intent = intent, + ) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/BaseActivity.kt b/app/src/main/java/io/github/wulkanowy/ui/base/BaseActivity.kt index 075557a5c..29996db7c 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/BaseActivity.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/BaseActivity.kt @@ -4,12 +4,14 @@ import android.app.ActivityManager import android.os.Bundle import android.view.View import android.widget.Toast -import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.viewbinding.ViewBinding +import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar.LENGTH_LONG import io.github.wulkanowy.R +import io.github.wulkanowy.ui.modules.auth.AuthDialog +import io.github.wulkanowy.ui.modules.captcha.CaptchaDialog import io.github.wulkanowy.ui.modules.login.LoginActivity import io.github.wulkanowy.utils.FragmentLifecycleLogger import io.github.wulkanowy.utils.getThemeAttrColor @@ -30,6 +32,8 @@ abstract class BaseActivity, VB : ViewBinding> : protected var messageContainer: View? = null + protected var messageAnchor: View? = null + abstract var presenter: T override fun onCreate(savedInstanceState: Bundle?) { @@ -48,6 +52,7 @@ abstract class BaseActivity, VB : ViewBinding> : if (messageContainer != null) { Snackbar.make(messageContainer!!, text, LENGTH_LONG) .setAction(R.string.all_details) { showErrorDetailsDialog(error) } + .apply { messageAnchor?.let { anchorView = it } } .show() } else showMessage(text) } @@ -57,23 +62,44 @@ abstract class BaseActivity, VB : ViewBinding> : } override fun showMessage(text: String) { - if (messageContainer != null) Snackbar.make(messageContainer!!, text, LENGTH_LONG).show() - else Toast.makeText(this, text, Toast.LENGTH_LONG).show() + if (messageContainer != null) { + Snackbar.make(messageContainer!!, text, LENGTH_LONG) + .apply { messageAnchor?.let { anchorView = it } } + .show() + } else Toast.makeText(this, text, Toast.LENGTH_LONG).show() } - override fun showExpiredDialog() { - AlertDialog.Builder(this) - .setTitle(R.string.main_session_expired) - .setMessage(R.string.main_session_relogin) - .setPositiveButton(R.string.main_log_in) { _, _ -> presenter.onExpiredLoginSelected() } + override fun showExpiredCredentialsDialog() { + MaterialAlertDialogBuilder(this) + .setTitle(R.string.main_expired_credentials_title) + .setMessage(R.string.main_expired_credentials_description) + .setPositiveButton(R.string.main_log_in) { _, _ -> presenter.onConfirmExpiredCredentialsSelected() } .setNegativeButton(android.R.string.cancel) { _, _ -> } .show() } + override fun onCaptchaVerificationRequired(url: String?) { + CaptchaDialog.newInstance(url).show(supportFragmentManager, "captcha_dialog") + } + + override fun showDecryptionFailedDialog() { + MaterialAlertDialogBuilder(this) + .setTitle(R.string.main_session_expired) + .setMessage(R.string.main_session_relogin) + .setPositiveButton(R.string.main_log_in) { _, _ -> presenter.onConfirmDecryptionFailedSelected() } + .setNegativeButton(android.R.string.cancel) { _, _ -> } + .show() + } + + override fun showAuthDialog() { + AuthDialog.newInstance().show(supportFragmentManager, "auth_dialog") + } + override fun showChangePasswordSnackbar(redirectUrl: String) { messageContainer?.let { Snackbar.make(it, R.string.error_password_change_required, LENGTH_LONG) .setAction(R.string.all_change) { openInternetBrowser(redirectUrl) } + .apply { messageAnchor?.let { anchorView = it } } .show() } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/BaseDialogFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/base/BaseDialogFragment.kt index 25a53395d..e63887b8f 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/BaseDialogFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/BaseDialogFragment.kt @@ -1,8 +1,13 @@ package io.github.wulkanowy.ui.base +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup import android.widget.Toast import androidx.fragment.app.DialogFragment import androidx.viewbinding.ViewBinding +import com.google.android.material.elevation.SurfaceColors import io.github.wulkanowy.utils.AnalyticsHelper import io.github.wulkanowy.utils.lifecycleAwareVariable import javax.inject.Inject @@ -22,8 +27,16 @@ abstract class BaseDialogFragment : DialogFragment(), BaseView Toast.makeText(context, text, Toast.LENGTH_LONG).show() } - override fun showExpiredDialog() { - (activity as? BaseActivity<*, *>)?.showExpiredDialog() + override fun showExpiredCredentialsDialog() { + (activity as? BaseActivity<*, *>)?.showExpiredCredentialsDialog() + } + + override fun onCaptchaVerificationRequired(url: String?) { + (activity as? BaseActivity<*, *>)?.onCaptchaVerificationRequired(url) + } + + override fun showDecryptionFailedDialog() { + (activity as? BaseActivity<*, *>)?.showDecryptionFailedDialog() } override fun openClearLoginView() { @@ -34,10 +47,25 @@ abstract class BaseDialogFragment : DialogFragment(), BaseView (activity as? BaseActivity<*, *>)?.showChangePasswordSnackbar(redirectUrl) } + override fun showAuthDialog() { + (activity as? BaseActivity<*, *>)?.showAuthDialog() + } + override fun showErrorDetailsDialog(error: Throwable) { ErrorDialog.newInstance(error).show(childFragmentManager, error.toString()) } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + view.setBackgroundColor(SurfaceColors.SURFACE_3.getColor(requireContext())) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ) = binding.root + override fun onResume() { super.onResume() analyticsHelper.setCurrentScreen(requireActivity(), this::class.simpleName) diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/BaseFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/base/BaseFragment.kt index dbc5af3a9..ba346131c 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/BaseFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/BaseFragment.kt @@ -38,8 +38,20 @@ abstract class BaseFragment(@LayoutRes layoutId: Int) : Fragme } } - override fun showExpiredDialog() { - (activity as? BaseActivity<*, *>)?.showExpiredDialog() + override fun showExpiredCredentialsDialog() { + (activity as? BaseActivity<*, *>)?.showExpiredCredentialsDialog() + } + + override fun onCaptchaVerificationRequired(url: String?) { + (activity as? BaseActivity<*, *>)?.onCaptchaVerificationRequired(url) + } + + override fun showDecryptionFailedDialog() { + (activity as? BaseActivity<*, *>)?.showDecryptionFailedDialog() + } + + override fun showAuthDialog() { + (activity as? BaseActivity<*, *>)?.showAuthDialog() } override fun openClearLoginView() { diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/BasePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/base/BasePresenter.kt index 15c069f54..d4cb20cac 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,10 +1,15 @@ package io.github.wulkanowy.ui.base import io.github.wulkanowy.data.repositories.StudentRepository -import kotlinx.coroutines.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancelChildren import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.launch import timber.log.Timber open class BasePresenter( @@ -23,19 +28,38 @@ open class BasePresenter( this.view = view errorHandler.apply { showErrorMessage = view::showError - onSessionExpired = view::showExpiredDialog + onExpiredCredentials = view::showExpiredCredentialsDialog + onCaptchaVerificationRequired = view::onCaptchaVerificationRequired + onDecryptionFailed = view::showDecryptionFailedDialog onNoCurrentStudent = view::openClearLoginView onPasswordChangeRequired = view::showChangePasswordSnackbar + onAuthorizationRequired = view::showAuthDialog } } - fun onExpiredLoginSelected() { - Timber.i("Attempt to switch the student after the session expires") + fun onConfirmDecryptionFailedSelected() { + Timber.i("Attempt to clear all data") + + presenterScope.launch { + runCatching { studentRepository.clearAll() } + .onFailure { + Timber.i("Clear data result: An exception occurred") + errorHandler.dispatch(it) + } + .onSuccess { + Timber.i("Clear data result: Open login view") + view?.openClearLoginView() + } + } + } + + fun onConfirmExpiredCredentialsSelected() { + Timber.i("Attempt to delete students associated with the account and switch to new student") presenterScope.launch { runCatching { val student = studentRepository.getCurrentStudent(false) - studentRepository.logoutStudent(student) + studentRepository.deleteStudentsAssociatedWithAccount(student) val students = studentRepository.getSavedStudents(false) if (students.isNotEmpty()) { @@ -44,11 +68,11 @@ open class BasePresenter( } } .onFailure { - Timber.i("Switch student result: An exception occurred") + Timber.i("Delete students result: An exception occurred") errorHandler.dispatch(it) } .onSuccess { - Timber.i("Switch student result: Open login view") + Timber.i("Delete students result: Open login view") view?.openClearLoginView() } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/BaseView.kt b/app/src/main/java/io/github/wulkanowy/ui/base/BaseView.kt index d3165ea44..88d5754f8 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/BaseView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/BaseView.kt @@ -6,7 +6,13 @@ interface BaseView { fun showMessage(text: String) - fun showExpiredDialog() + fun showExpiredCredentialsDialog() + + fun onCaptchaVerificationRequired(url: String?) + + fun showDecryptionFailedDialog() + + fun showAuthDialog() fun openClearLoginView() 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 48c003b7e..679d904a3 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 @@ -4,27 +4,32 @@ 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.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.data.repositories.PreferencesRepository import io.github.wulkanowy.databinding.DialogErrorBinding import io.github.wulkanowy.utils.* import javax.inject.Inject @AndroidEntryPoint -class ErrorDialog : DialogFragment() { +class ErrorDialog : BaseDialogFragment() { @Inject lateinit var appInfo: AppInfo + @Inject + lateinit var preferencesRepository: PreferencesRepository + + private lateinit var error: Throwable + companion object { private const val ARGUMENT_KEY = "error" @@ -33,32 +38,31 @@ class ErrorDialog : DialogFragment() { } } - 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 onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + error = requireArguments().serializable(ARGUMENT_KEY) } - private fun getAlertDialog(binding: DialogErrorBinding, error: Throwable): AlertDialog { + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { return MaterialAlertDialogBuilder(requireContext()).apply { val errorStacktrace = error.stackTraceToString() setTitle(R.string.all_details) - setView(binding.root) + setView(DialogErrorBinding.inflate(layoutInflater).apply { binding = this }.root) setNeutralButton(R.string.about_feedback) { _, _ -> openConfirmDialog { openEmailClient(errorStacktrace) } } setNegativeButton(android.R.string.cancel) { _, _ -> } setPositiveButton(android.R.string.copy) { _, _ -> copyErrorToClipboard(errorStacktrace) } - }.create() + }.create().apply { + setOnShowListener { + getButton(AlertDialog.BUTTON_NEUTRAL).isEnabled = error.isShouldBeReported() + } + } } - private fun DialogErrorBinding.bindErrorDetails(error: Throwable) { - return with(this) { + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + with(binding) { errorDialogHumanizedMessage.text = resources.getErrorString(error) errorDialogErrorMessage.text = error.localizedMessage errorDialogErrorMessage.isGone = error.localizedMessage.isNullOrBlank() @@ -67,12 +71,6 @@ class ErrorDialog : DialogFragment() { } } - 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) @@ -80,7 +78,7 @@ class ErrorDialog : DialogFragment() { } private fun openConfirmDialog(callback: () -> Unit) { - AlertDialog.Builder(requireContext()) + MaterialAlertDialogBuilder(requireContext()) .setTitle(R.string.dialog_error_check_update) .setMessage(R.string.dialog_error_check_update_message) .setNeutralButton(R.string.about_feedback) { _, _ -> callback() } @@ -99,7 +97,8 @@ class ErrorDialog : DialogFragment() { R.string.about_feedback_template, "${appInfo.systemManufacturer} ${appInfo.systemModel}", appInfo.systemVersion.toString(), - "${appInfo.versionName}-${appInfo.buildFlavor}" + "${appInfo.versionName}-${appInfo.buildFlavor}", + preferencesRepository.installationId, ) + "\n" + content, onActivityNotFound = { requireContext().openInternetBrowser( @@ -109,8 +108,4 @@ class ErrorDialog : DialogFragment() { } ) } - - 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 afe200e9a..e17c0c9ec 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 @@ -3,6 +3,8 @@ package io.github.wulkanowy.ui.base import android.content.Context import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.data.exceptions.NoCurrentStudentException +import io.github.wulkanowy.sdk.scrapper.exception.AuthorizationRequiredException +import io.github.wulkanowy.sdk.scrapper.exception.CloudflareVerificationException import io.github.wulkanowy.sdk.scrapper.login.BadCredentialsException import io.github.wulkanowy.sdk.scrapper.login.PasswordChangeRequiredException import io.github.wulkanowy.utils.getErrorString @@ -14,12 +16,18 @@ open class ErrorHandler @Inject constructor(@ApplicationContext protected val co var showErrorMessage: (String, Throwable) -> Unit = { _, _ -> } - var onSessionExpired: () -> Unit = {} + var onExpiredCredentials: () -> Unit = {} + + var onDecryptionFailed: () -> Unit = {} var onNoCurrentStudent: () -> Unit = {} var onPasswordChangeRequired: (String) -> Unit = {} + var onAuthorizationRequired: () -> Unit = {} + + var onCaptchaVerificationRequired: (url: String?) -> Unit = {} + fun dispatch(error: Throwable) { Timber.e(error, "An exception occurred while the Wulkanowy was running") proceed(error) @@ -29,15 +37,20 @@ open class ErrorHandler @Inject constructor(@ApplicationContext protected val co showErrorMessage(context.resources.getErrorString(error), error) when (error) { is PasswordChangeRequiredException -> onPasswordChangeRequired(error.redirectUrl) - is ScramblerException, is BadCredentialsException -> onSessionExpired() + is ScramblerException -> onDecryptionFailed() + is BadCredentialsException -> onExpiredCredentials() is NoCurrentStudentException -> onNoCurrentStudent() + is AuthorizationRequiredException -> onAuthorizationRequired() + is CloudflareVerificationException -> onCaptchaVerificationRequired(error.originalUrl) } } open fun clear() { showErrorMessage = { _, _ -> } - onSessionExpired = {} + onExpiredCredentials = {} + onDecryptionFailed = {} onNoCurrentStudent = {} onPasswordChangeRequired = {} + onAuthorizationRequired = {} } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/ThemeManager.kt b/app/src/main/java/io/github/wulkanowy/ui/base/ThemeManager.kt index 2d83bbbf9..f42f315ce 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/ThemeManager.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/ThemeManager.kt @@ -1,17 +1,19 @@ package io.github.wulkanowy.ui.base +import android.content.pm.PackageInfo +import android.content.pm.PackageManager import android.content.pm.PackageManager.GET_ACTIVITIES +import android.os.Build import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatDelegate -import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM -import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_NO -import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_YES +import com.google.android.material.color.DynamicColors import io.github.wulkanowy.R import io.github.wulkanowy.data.enums.AppTheme import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.ui.modules.login.LoginActivity +import io.github.wulkanowy.ui.modules.luckynumberwidget.LuckyNumberWidgetConfigureActivity import io.github.wulkanowy.ui.modules.main.MainActivity -import io.github.wulkanowy.ui.modules.message.send.SendMessageActivity +import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetConfigureActivity import javax.inject.Inject import javax.inject.Singleton @@ -25,31 +27,40 @@ class ThemeManager @Inject constructor(private val preferencesRepository: Prefer when (activity) { is MainActivity -> activity.setTheme(R.style.WulkanowyTheme_Black) is LoginActivity -> activity.setTheme(R.style.WulkanowyTheme_Login_Black) - is SendMessageActivity -> activity.setTheme(R.style.WulkanowyTheme_MessageSend_Black) } } + } else if (activity is TimetableWidgetConfigureActivity || activity is LuckyNumberWidgetConfigureActivity) { + DynamicColors.applyToActivityIfAvailable(activity) } } fun applyDefaultTheme() { AppCompatDelegate.setDefaultNightMode( when (preferencesRepository.appTheme) { - AppTheme.LIGHT -> MODE_NIGHT_NO - AppTheme.DARK, AppTheme.BLACK -> MODE_NIGHT_YES - AppTheme.SYSTEM -> MODE_NIGHT_FOLLOW_SYSTEM + AppTheme.LIGHT -> AppCompatDelegate.MODE_NIGHT_NO + AppTheme.DARK, AppTheme.BLACK -> AppCompatDelegate.MODE_NIGHT_YES + AppTheme.SYSTEM -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM } ) } - private fun isThemeApplicable(activity: AppCompatActivity) = - activity.packageManager - .getPackageInfo(activity.packageName, GET_ACTIVITIES) + private fun isThemeApplicable(activity: AppCompatActivity): Boolean = + getPackageInfo(activity) .activities .singleOrNull { it.name == activity::class.java.canonicalName } ?.theme .let { it == R.style.WulkanowyTheme_Black || it == R.style.WulkanowyTheme_NoActionBar || it == R.style.WulkanowyTheme_Login || it == R.style.WulkanowyTheme_Login_Black - || it == R.style.WulkanowyTheme_MessageSend || it == R.style.WulkanowyTheme_MessageSend_Black } + + @Suppress("DEPRECATION") + private fun getPackageInfo(activity: AppCompatActivity): PackageInfo { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + activity.packageManager.getPackageInfo( + activity.packageName, + PackageManager.PackageInfoFlags.of(GET_ACTIVITIES.toLong()) + ) + } else activity.packageManager.getPackageInfo(activity.packageName, GET_ACTIVITIES) + } } 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 561419a05..f0969fac4 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 @@ -9,11 +9,14 @@ 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.luckynumber.history.LuckyNumberHistoryFragment import io.github.wulkanowy.ui.modules.message.MessageFragment +import io.github.wulkanowy.ui.modules.mobiledevice.MobileDeviceFragment import io.github.wulkanowy.ui.modules.more.MoreFragment import io.github.wulkanowy.ui.modules.note.NoteFragment -import io.github.wulkanowy.ui.modules.schoolandteachers.school.SchoolFragment +import io.github.wulkanowy.ui.modules.schoolandteachers.SchoolAndTeachersFragment import io.github.wulkanowy.ui.modules.schoolannouncement.SchoolAnnouncementFragment +import io.github.wulkanowy.ui.modules.settings.SettingsFragment import io.github.wulkanowy.ui.modules.timetable.TimetableFragment import kotlinx.serialization.Serializable import java.time.LocalDate @@ -39,10 +42,13 @@ sealed class Destination { NOTE(Note), CONFERENCE(Conference), SCHOOL_ANNOUNCEMENT(SchoolAnnouncement), - SCHOOL(School), - LUCKY_NUMBER(More), + SCHOOL_AND_TEACHERS(SchoolAndTeachers), + LUCKY_NUMBER(LuckyNumber), + LUCKY_NUMBER_HISTORY(LuckyNumberHistory), MORE(More), - MESSAGE(Message); + MESSAGE(Message), + MOBILE_DEVICE(MobileDevice), + SETTINGS(Settings); } @Serializable @@ -103,9 +109,9 @@ sealed class Destination { } @Serializable - object School : Destination() { - override val destinationType get() = Type.SCHOOL - override val destinationFragment get() = SchoolFragment.newInstance() + object SchoolAndTeachers : Destination() { + override val destinationType get() = Type.SCHOOL_AND_TEACHERS + override val destinationFragment get() = SchoolAndTeachersFragment.newInstance() } @Serializable @@ -114,6 +120,12 @@ sealed class Destination { override val destinationFragment get() = LuckyNumberFragment.newInstance() } + @Serializable + object LuckyNumberHistory : Destination() { + override val destinationType get() = Type.LUCKY_NUMBER_HISTORY + override val destinationFragment get() = LuckyNumberHistoryFragment.newInstance() + } + @Serializable object More : Destination() { override val destinationType get() = Type.MORE @@ -125,4 +137,16 @@ sealed class Destination { override val destinationType get() = Type.MESSAGE override val destinationFragment get() = MessageFragment.newInstance() } + + @Serializable + object MobileDevice : Destination() { + override val destinationType get() = Type.MOBILE_DEVICE + override val destinationFragment get() = MobileDeviceFragment.newInstance() + } + + @Serializable + object Settings : Destination() { + override val destinationType get() = Type.SETTINGS + override val destinationFragment get() = SettingsFragment.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 701656b55..d7f39e303 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 @@ -6,6 +6,7 @@ import android.view.View import androidx.recyclerview.widget.LinearLayoutManager import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R +import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.databinding.FragmentAboutBinding import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.modules.about.contributor.ContributorFragment @@ -30,6 +31,9 @@ class AboutFragment : BaseFragment(R.layout.fragment_about @Inject lateinit var appInfo: AppInfo + @Inject + lateinit var preferencesRepository: PreferencesRepository + override val versionRes: Triple? get() = context?.run { val buildTimestamp = @@ -185,7 +189,8 @@ class AboutFragment : BaseFragment(R.layout.fragment_about R.string.about_feedback_template, "${appInfo.systemManufacturer} ${appInfo.systemModel}", appInfo.systemVersion.toString(), - "${appInfo.versionName}-${appInfo.buildFlavor}" + "${appInfo.versionName}-${appInfo.buildFlavor}", + preferencesRepository.installationId, ), onActivityNotFound = { requireContext().openInternetBrowser( diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/account/AccountFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/account/AccountFragment.kt index 051c93c95..f115372a5 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/account/AccountFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/account/AccountFragment.kt @@ -34,6 +34,7 @@ class AccountFragment : BaseFragment(R.layout.fragment_a override val titleStringId = R.string.account_title + @Suppress("DEPRECATION") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setHasOptionsMenu(true) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountdetails/AccountDetailsFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountdetails/AccountDetailsFragment.kt index c3137ec58..d6bc6154b 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountdetails/AccountDetailsFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountdetails/AccountDetailsFragment.kt @@ -6,8 +6,10 @@ import android.view.MenuInflater import android.view.MenuItem import android.view.View import androidx.appcompat.app.AlertDialog +import androidx.core.os.bundleOf import androidx.core.view.get import androidx.core.view.isVisible +import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Student @@ -21,6 +23,7 @@ import io.github.wulkanowy.ui.modules.studentinfo.StudentInfoFragment import io.github.wulkanowy.ui.modules.studentinfo.StudentInfoView import io.github.wulkanowy.utils.createNameInitialsDrawable import io.github.wulkanowy.utils.nickOrName +import io.github.wulkanowy.utils.serializable import javax.inject.Inject @AndroidEntryPoint @@ -37,12 +40,12 @@ class AccountDetailsFragment : private const val ARGUMENT_KEY = "Data" - fun newInstance(student: Student) = - AccountDetailsFragment().apply { - arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, student) } - } + fun newInstance(student: Student) = AccountDetailsFragment().apply { + arguments = bundleOf(ARGUMENT_KEY to student) + } } + @Suppress("DEPRECATION") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setHasOptionsMenu(true) @@ -51,7 +54,7 @@ class AccountDetailsFragment : override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding = FragmentAccountDetailsBinding.bind(view) - presenter.onAttachView(this, requireArguments()[ARGUMENT_KEY] as Student) + presenter.onAttachView(this, requireArguments().serializable(ARGUMENT_KEY)) } override fun initView() { @@ -112,7 +115,7 @@ class AccountDetailsFragment : override fun showLogoutConfirmDialog() { context?.let { - AlertDialog.Builder(it) + MaterialAlertDialogBuilder(it) .setTitle(R.string.account_logout_student) .setMessage(R.string.account_confirm) .setPositiveButton(R.string.account_logout) { _, _ -> presenter.onLogoutConfirm() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountedit/AccountEditDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountedit/AccountEditDialog.kt index 21a7a492d..4229579c0 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountedit/AccountEditDialog.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountedit/AccountEditDialog.kt @@ -1,14 +1,16 @@ package io.github.wulkanowy.ui.modules.account.accountedit +import android.app.Dialog import android.os.Bundle -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup +import androidx.core.os.bundleOf import androidx.recyclerview.widget.GridLayoutManager +import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.databinding.DialogAccountEditBinding import io.github.wulkanowy.ui.base.BaseDialogFragment +import io.github.wulkanowy.utils.serializable import javax.inject.Inject @AndroidEntryPoint @@ -24,28 +26,21 @@ class AccountEditDialog : BaseDialogFragment(), Accoun private const val ARGUMENT_KEY = "student_with_semesters" - fun newInstance(student: Student) = - AccountEditDialog().apply { - arguments = Bundle().apply { - putSerializable(ARGUMENT_KEY, student) - } - } + fun newInstance(student: Student) = AccountEditDialog().apply { + arguments = bundleOf(ARGUMENT_KEY to student) + } } - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setStyle(STYLE_NO_TITLE, 0) - } - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View = DialogAccountEditBinding.inflate(inflater).apply { binding = this }.root + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return MaterialAlertDialogBuilder(requireContext(), theme) + .setView(DialogAccountEditBinding.inflate(layoutInflater).apply { binding = this }.root) + .create() + } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - presenter.onAttachView(this, requireArguments()[ARGUMENT_KEY] as Student) + presenter.onAttachView(this, requireArguments().serializable(ARGUMENT_KEY)) } override fun initView() { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountquick/AccountQuickDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountquick/AccountQuickDialog.kt index 4279102e1..2d2dccec4 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountquick/AccountQuickDialog.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountquick/AccountQuickDialog.kt @@ -1,10 +1,11 @@ package io.github.wulkanowy.ui.modules.account.accountquick +import android.app.Dialog import android.os.Bundle -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup +import androidx.core.os.bundleOf import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.databinding.DialogAccountQuickBinding @@ -13,6 +14,7 @@ import io.github.wulkanowy.ui.modules.account.AccountAdapter import io.github.wulkanowy.ui.modules.account.AccountFragment import io.github.wulkanowy.ui.modules.account.AccountItem import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.utils.serializable import javax.inject.Inject @AndroidEntryPoint @@ -30,27 +32,23 @@ class AccountQuickDialog : BaseDialogFragment(), Acco fun newInstance(studentsWithSemesters: List) = AccountQuickDialog().apply { - arguments = Bundle().apply { - putSerializable(STUDENTS_ARGUMENT_KEY, studentsWithSemesters.toTypedArray()) - } + arguments = bundleOf(STUDENTS_ARGUMENT_KEY to studentsWithSemesters.toTypedArray()) } } - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setStyle(STYLE_NO_TITLE, 0) + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return MaterialAlertDialogBuilder(requireContext(), theme) + .setView( + DialogAccountQuickBinding.inflate(layoutInflater) + .apply { binding = this }.root + ) + .create() } - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ) = DialogAccountQuickBinding.inflate(inflater).apply { binding = this }.root - - @Suppress("UNCHECKED_CAST") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - val studentsWithSemesters = - (requireArguments()[STUDENTS_ARGUMENT_KEY] as Array).toList() + super.onViewCreated(view, savedInstanceState) + val studentsWithSemesters = requireArguments() + .serializable>(STUDENTS_ARGUMENT_KEY).toList() presenter.onAttachView(this, studentsWithSemesters) } 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 39f376f65..4e9baac3a 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceAdapter.kt @@ -1,5 +1,6 @@ package io.github.wulkanowy.ui.modules.attendance +import android.graphics.Typeface import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -10,6 +11,7 @@ import io.github.wulkanowy.data.db.entities.Attendance import io.github.wulkanowy.data.enums.SentExcuseStatus import io.github.wulkanowy.databinding.ItemAttendanceBinding import io.github.wulkanowy.utils.descriptionRes +import io.github.wulkanowy.utils.getThemeAttrColor import io.github.wulkanowy.utils.isExcusableOrNotExcused import javax.inject.Inject @@ -39,7 +41,33 @@ class AttendanceAdapter @Inject constructor() : root.context.getString(R.string.all_no_data) } attendanceItemDescription.setText(item.descriptionRes) - attendanceItemAlert.isVisible = item.let { it.absence && !it.excused } + + attendanceItemDescription.setTextColor( + root.context.getThemeAttrColor( + when { + item.absence && !item.excused -> R.attr.colorAttendanceAbsence + item.lateness && !item.excused -> R.attr.colorAttendanceLateness + else -> android.R.attr.textColorSecondary + } + ) + ) + + if (item.exemption || item.excused) { + attendanceItemDescription.setTypeface(null, Typeface.BOLD) + } else { + attendanceItemDescription.setTypeface(null, Typeface.NORMAL) + } + + attendanceItemAlert.isVisible = + item.let { (it.absence && !it.excused) || (it.lateness && !it.excused) } + + attendanceItemAlert.setColorFilter(root.context.getThemeAttrColor( + when{ + item.absence && !item.excused -> R.attr.colorAttendanceAbsence + item.lateness && !item.excused -> R.attr.colorAttendanceLateness + else -> android.R.attr.colorPrimary + } + )) attendanceItemNumber.visibility = View.GONE attendanceItemExcuseInfo.visibility = View.GONE attendanceItemExcuseCheckbox.visibility = View.GONE @@ -54,10 +82,12 @@ class AttendanceAdapter @Inject constructor() : attendanceItemExcuseInfo.visibility = View.VISIBLE attendanceItemAlert.visibility = View.INVISIBLE } + SentExcuseStatus.DENIED -> { attendanceItemExcuseInfo.setImageResource(R.drawable.ic_excuse_denied) attendanceItemExcuseInfo.visibility = View.VISIBLE } + else -> { if (item.isExcusableOrNotExcused && excuseActionMode) { attendanceItemNumber.visibility = View.GONE diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceDialog.kt index 9b5c63e4c..635033132 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceDialog.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceDialog.kt @@ -1,19 +1,22 @@ package io.github.wulkanowy.ui.modules.attendance +import android.app.Dialog import android.os.Bundle -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.DialogFragment +import androidx.core.os.bundleOf +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import dagger.hilt.android.AndroidEntryPoint +import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Attendance import io.github.wulkanowy.databinding.DialogAttendanceBinding +import io.github.wulkanowy.ui.base.BaseDialogFragment import io.github.wulkanowy.utils.descriptionRes -import io.github.wulkanowy.utils.lifecycleAwareVariable +import io.github.wulkanowy.utils.getThemeAttrColor +import io.github.wulkanowy.utils.serializable import io.github.wulkanowy.utils.toFormattedString -class AttendanceDialog : DialogFragment() { - - private var binding: DialogAttendanceBinding by lifecycleAwareVariable() +@AndroidEntryPoint +class AttendanceDialog : BaseDialogFragment() { private lateinit var attendance: Attendance @@ -22,23 +25,20 @@ class AttendanceDialog : DialogFragment() { private const val ARGUMENT_KEY = "Item" fun newInstance(exam: Attendance) = AttendanceDialog().apply { - arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, exam) } + arguments = bundleOf(ARGUMENT_KEY to exam) } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setStyle(STYLE_NO_TITLE, 0) - arguments?.run { - attendance = getSerializable(ARGUMENT_KEY) as Attendance - } + attendance = requireArguments().serializable(ARGUMENT_KEY) } - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ) = DialogAttendanceBinding.inflate(inflater).apply { binding = this }.root + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return MaterialAlertDialogBuilder(requireContext(), theme) + .setView(DialogAttendanceBinding.inflate(layoutInflater).apply { binding = this }.root) + .create() + } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -46,6 +46,16 @@ class AttendanceDialog : DialogFragment() { with(binding) { attendanceDialogSubjectValue.text = attendance.subject attendanceDialogDescriptionValue.setText(attendance.descriptionRes) + attendanceDialogDescriptionValue.setTextColor( + root.context.getThemeAttrColor( + when { + attendance.absence && !attendance.excused -> R.attr.colorAttendanceAbsence + attendance.lateness && !attendance.excused -> R.attr.colorAttendanceLateness + else -> android.R.attr.textColorSecondary + } + ) + ) + attendanceDialogDateValue.text = attendance.date.toFormattedString() attendanceDialogNumberValue.text = attendance.number.toString() attendanceDialogClose.setOnClickListener { dismiss() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceFragment.kt index 6354b5e04..6e842b4d7 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 @@ -4,10 +4,10 @@ import android.content.DialogInterface.BUTTON_POSITIVE import android.os.Bundle 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.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Attendance @@ -84,6 +84,7 @@ class AttendanceFragment : BaseFragment(R.layout.frag } } + @Suppress("DEPRECATION") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setHasOptionsMenu(true) @@ -123,7 +124,7 @@ class AttendanceFragment : BaseFragment(R.layout.frag attendanceExcuseButton.setOnClickListener { presenter.onExcuseButtonClick() } - attendanceNavContainer.elevation = requireContext().dpToPx(8f) + attendanceNavContainer.elevation = requireContext().dpToPx(3f) } } @@ -147,6 +148,10 @@ class AttendanceFragment : BaseFragment(R.layout.frag binding.attendanceNavDate.text = date } + override fun showNavigation(show: Boolean) { + binding.attendanceNavContainer.isVisible = show + } + override fun clearData() { with(attendanceAdapter) { items = emptyList() @@ -227,7 +232,7 @@ class AttendanceFragment : BaseFragment(R.layout.frag override fun showExcuseDialog() { val dialogBinding = DialogExcuseBinding.inflate(LayoutInflater.from(context)) - AlertDialog.Builder(requireContext()) + MaterialAlertDialogBuilder(requireContext()) .setTitle(R.string.attendance_excuse_title) .setView(dialogBinding.root) .setNegativeButton(android.R.string.cancel) { _, _ -> } @@ -280,7 +285,9 @@ class AttendanceFragment : BaseFragment(R.layout.frag override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) - outState.putLong(SAVED_DATE_KEY, presenter.currentDate.toEpochDay()) + presenter.currentDate?.let { + outState.putLong(SAVED_DATE_KEY, it.toEpochDay()) + } } override fun onDestroyView() { 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 26bfaf19f..f66479daf 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 @@ -3,10 +3,14 @@ package io.github.wulkanowy.ui.modules.attendance import android.annotation.SuppressLint import io.github.wulkanowy.data.* import io.github.wulkanowy.data.db.entities.Attendance +import io.github.wulkanowy.data.db.entities.Semester +import io.github.wulkanowy.data.db.entities.Student +import io.github.wulkanowy.data.db.entities.Timetable import io.github.wulkanowy.data.repositories.AttendanceRepository import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.SemesterRepository import io.github.wulkanowy.data.repositories.StudentRepository +import io.github.wulkanowy.data.repositories.TimetableRepository import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.utils.* @@ -14,6 +18,7 @@ import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.onEach import timber.log.Timber +import java.time.DayOfWeek import java.time.LocalDate import java.time.LocalDate.now import java.time.LocalDate.ofEpochDay @@ -28,9 +33,10 @@ class AttendancePresenter @Inject constructor( private val analytics: AnalyticsHelper ) : BasePresenter(errorHandler, studentRepository) { - private var baseDate: LocalDate = now().previousOrSameSchoolDay + private var initialDate: LocalDate? = null + private var isWeekendHasLessons: Boolean = false - lateinit var currentDate: LocalDate + var currentDate: LocalDate? = null private set private lateinit var lastError: Throwable @@ -44,27 +50,34 @@ class AttendancePresenter @Inject constructor( view.initView() Timber.i("Attendance view was initialized") errorHandler.showErrorMessage = ::showErrorViewOnError - reloadView(ofEpochDay(date ?: baseDate.toEpochDay())) + currentDate = date?.let(::ofEpochDay) loadData() - if (currentDate.isHolidays) setBaseDateOnHolidays() } fun onPreviousDay() { + val date = if (isWeekendHasLessons) { + currentDate?.minusDays(1) + } else currentDate?.previousSchoolDay + view?.finishActionMode() attendanceToExcuseList.clear() - reloadView(currentDate.previousSchoolDay) + reloadView(date ?: return) loadData() } fun onNextDay() { + val date = if (isWeekendHasLessons) { + currentDate?.plusDays(1) + } else currentDate?.nextSchoolDay + view?.finishActionMode() attendanceToExcuseList.clear() - reloadView(currentDate.nextSchoolDay) + reloadView(date ?: return) loadData() } fun onPickDate() { - view?.showDatePickerDialog(currentDate) + view?.showDatePickerDialog(currentDate ?: return) } fun onDateSet(year: Int, month: Int, day: Int) { @@ -93,10 +106,8 @@ class AttendancePresenter @Inject constructor( Timber.i("Attendance view is reselected") view?.let { view -> if (view.currentStackSize == 1) { - baseDate = now().previousOrSameSchoolDay - - if (currentDate != baseDate) { - reloadView(baseDate) + if (currentDate != initialDate) { + reloadView(initialDate ?: return) loadData() } else if (!view.isViewEmpty) { view.resetView() @@ -188,19 +199,6 @@ class AttendancePresenter @Inject constructor( return true } - private fun setBaseDateOnHolidays() { - 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") - } - private fun loadData(forceRefresh: Boolean = false) { Timber.i("Loading attendance data started") @@ -211,11 +209,13 @@ class AttendancePresenter @Inject constructor( isParent = student.isParent val semester = semesterRepository.getCurrentSemester(student) + + checkInitialAndCurrentDate(student, semester) attendanceRepository.getAttendance( student = student, semester = semester, - start = currentDate, - end = currentDate, + start = currentDate ?: now(), + end = currentDate ?: now(), forceRefresh = forceRefresh ) } @@ -231,6 +231,8 @@ class AttendancePresenter @Inject constructor( }.sortedBy { item -> item.number } } .onResourceData { + isWeekendHasLessons = isWeekendHasLessons || isWeekendHasLessons(it) + view?.run { enableSwipe(true) showProgress(false) @@ -238,6 +240,7 @@ class AttendancePresenter @Inject constructor( showEmpty(it.isEmpty()) showContent(it.isNotEmpty()) updateData(it) + reloadNavigation() } } .onResourceIntermediate { view?.showRefresh(true) } @@ -263,6 +266,43 @@ class AttendancePresenter @Inject constructor( .launch() } + private suspend fun checkInitialAndCurrentDate(student: Student, semester: Semester) { + if (initialDate == null) { + val lessons = attendanceRepository.getAttendance( + student = student, + semester = semester, + start = now().monday, + end = now().sunday, + forceRefresh = false, + ).toFirstResult().dataOrNull.orEmpty() + isWeekendHasLessons = isWeekendHasLessons(lessons) + initialDate = getInitialDate(semester) + } + + if (currentDate == null) { + currentDate = initialDate + } + } + + private fun isWeekendHasLessons( + lessons: List, + ): Boolean = lessons.any { + it.date.dayOfWeek in listOf( + DayOfWeek.SATURDAY, + DayOfWeek.SUNDAY, + ) + } + + private fun getInitialDate(semester: Semester): LocalDate { + val now = now() + + return when { + now.isHolidays -> now.getLastSchoolDayIfHoliday(semester.schoolYear) + isWeekendHasLessons -> now + else -> now.previousOrSameSchoolDay + } + } + private fun excuseAbsence(reason: String?, toExcuseList: List) { resourceFlow { val student = studentRepository.getCurrentStudent() @@ -311,7 +351,7 @@ class AttendancePresenter @Inject constructor( private fun reloadView(date: LocalDate) { currentDate = date - Timber.i("Reload attendance view with the date ${currentDate.toFormattedString()}") + Timber.i("Reload attendance view with the date ${currentDate?.toFormattedString()}") view?.apply { showProgress(true) enableSwipe(false) @@ -326,10 +366,13 @@ class AttendancePresenter @Inject constructor( @SuppressLint("DefaultLocale") private fun reloadNavigation() { + val currentDate = currentDate ?: return + view?.apply { showPreButton(!currentDate.minusDays(1).isHolidays) showNextButton(!currentDate.plusDays(1).isHolidays) updateNavigationDay(currentDate.toFormattedString("EEEE, dd.MM").capitalise()) + showNavigation(true) } } } 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 b0123065a..2629c217e 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 @@ -40,6 +40,8 @@ interface AttendanceView : BaseView { fun showContent(show: Boolean) + fun showNavigation(show: Boolean) + fun showPreButton(show: Boolean) fun showNextButton(show: Boolean) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/auth/AuthDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/auth/AuthDialog.kt new file mode 100644 index 000000000..fa29df473 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/auth/AuthDialog.kt @@ -0,0 +1,81 @@ +package io.github.wulkanowy.ui.modules.auth + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.text.parseAsHtml +import androidx.core.view.isVisible +import androidx.core.widget.doOnTextChanged +import dagger.hilt.android.AndroidEntryPoint +import io.github.wulkanowy.R +import io.github.wulkanowy.databinding.DialogAuthBinding +import io.github.wulkanowy.ui.base.BaseDialogFragment +import javax.inject.Inject + +@AndroidEntryPoint +class AuthDialog : BaseDialogFragment(), AuthView { + + @Inject + lateinit var presenter: AuthPresenter + + companion object { + fun newInstance() = AuthDialog() + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setStyle(STYLE_NO_TITLE, R.style.FullScreenDialogStyle) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + return DialogAuthBinding.inflate(inflater).apply { binding = this }.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + presenter.onAttachView(this) + + binding.authInput.doOnTextChanged { text, _, _, _ -> + presenter.onPeselChange(text?.toString()) + } + + binding.authButton.setOnClickListener { presenter.authorize() } + binding.authSuccessButton.setOnClickListener { + activity?.recreate() + dismiss() + } + binding.authButtonSkip.setOnClickListener { dismiss() } + } + + override fun enableAuthButton(isEnabled: Boolean) { + binding.authButton.isEnabled = isEnabled + } + + override fun showProgress(show: Boolean) { + binding.authProgress.isVisible = show + } + + override fun showPeselError(show: Boolean) { + binding.authInputLayout.error = getString(R.string.auth_api_error).takeIf { show } + } + + override fun showInvalidPeselError(show: Boolean) { + binding.authInputLayout.error = getString(R.string.auth_invalid_error).takeIf { show } + } + + override fun showSuccess(show: Boolean) { + binding.authSuccess.isVisible = show + } + + override fun showContent(show: Boolean) { + binding.authForm.isVisible = show + } + + override fun showDescriptionWithName(name: String) { + binding.authDescription.text = getString(R.string.auth_description, name).parseAsHtml() + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/auth/AuthPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/auth/AuthPresenter.kt new file mode 100644 index 000000000..8f579712b --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/auth/AuthPresenter.kt @@ -0,0 +1,100 @@ +package io.github.wulkanowy.ui.modules.auth + +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 kotlinx.coroutines.launch +import javax.inject.Inject + +class AuthPresenter @Inject constructor( + private val semesterRepository: SemesterRepository, + errorHandler: ErrorHandler, + studentRepository: StudentRepository +) : BasePresenter(errorHandler, studentRepository) { + + private var pesel: String = "" + + override fun onAttachView(view: AuthView) { + super.onAttachView(view) + view.enableAuthButton(pesel.length == 11) + view.showSuccess(false) + view.showProgress(false) + + loadName() + } + + private fun loadName() { + presenterScope.launch { + runCatching { studentRepository.getCurrentStudent(false) } + .onSuccess { view?.showDescriptionWithName(it.studentName) } + .onFailure { errorHandler.dispatch(it) } + } + } + + fun onPeselChange(newPesel: String?) { + pesel = newPesel.orEmpty() + + view?.enableAuthButton(pesel.length == 11) + view?.showPeselError(false) + view?.showInvalidPeselError(false) + } + + fun authorize() { + presenterScope.launch { + view?.showProgress(true) + view?.showContent(false) + + if (!isValidPESEL(pesel)) { + view?.showInvalidPeselError(true) + view?.showProgress(false) + view?.showContent(true) + return@launch + } + + runCatching { + val student = studentRepository.getCurrentStudent() + val semester = semesterRepository.getCurrentSemester(student) + + val isSuccess = studentRepository.authorizePermission(student, semester, pesel) + if (isSuccess) { + studentRepository.refreshStudentName(student, semester) + } + isSuccess + } + .onFailure { errorHandler.dispatch(it) } + .onSuccess { + if (it) { + view?.showSuccess(true) + view?.showContent(false) + view?.showPeselError(false) + } else { + view?.showSuccess(false) + view?.showContent(true) + view?.showPeselError(true) + } + } + + view?.showProgress(false) + } + } + + private fun isValidPESEL(peselString: String): Boolean { + if (peselString.length != 11) { + return false + } + + val weights = intArrayOf(1, 3, 7, 9, 1, 3, 7, 9, 1, 3) + var sum = 0 + + for (i in 0 until 10) { + sum += weights[i] * Character.getNumericValue(peselString[i]) + } + + sum %= 10 + sum = 10 - sum + sum %= 10 + + return sum == Character.getNumericValue(peselString[10]) + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/auth/AuthView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/auth/AuthView.kt new file mode 100644 index 000000000..d7e1917c2 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/auth/AuthView.kt @@ -0,0 +1,20 @@ +package io.github.wulkanowy.ui.modules.auth + +import io.github.wulkanowy.ui.base.BaseView + +interface AuthView : BaseView { + + fun enableAuthButton(isEnabled: Boolean) + + fun showProgress(show: Boolean) + + fun showPeselError(show: Boolean) + + fun showInvalidPeselError(show: Boolean) + + fun showSuccess(show: Boolean) + + fun showContent(show: Boolean) + + fun showDescriptionWithName(name: String) +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/captcha/CaptchaDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/captcha/CaptchaDialog.kt new file mode 100644 index 000000000..ed8293a9f --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/captcha/CaptchaDialog.kt @@ -0,0 +1,86 @@ +package io.github.wulkanowy.ui.modules.captcha + +import android.annotation.SuppressLint +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.webkit.WebView +import android.webkit.WebViewClient +import androidx.core.os.bundleOf +import dagger.hilt.android.AndroidEntryPoint +import io.github.wulkanowy.R +import io.github.wulkanowy.databinding.DialogCaptchaBinding +import io.github.wulkanowy.sdk.Sdk +import io.github.wulkanowy.ui.base.BaseDialogFragment +import timber.log.Timber +import javax.inject.Inject + +@AndroidEntryPoint +class CaptchaDialog : BaseDialogFragment() { + + @Inject + lateinit var sdk: Sdk + + private var webView: WebView? = null + + companion object { + const val CAPTCHA_SUCCESS = "captcha_success" + private const val CAPTCHA_URL = "captcha_url" + private const val CAPTCHA_CHECK_JS = "document.getElementById('challenge-running') == null" + + fun newInstance(url: String?): CaptchaDialog { + return CaptchaDialog().apply { + arguments = bundleOf(CAPTCHA_URL to url) + } + } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View = DialogCaptchaBinding.inflate(inflater).apply { binding = this }.root + + @SuppressLint("SetJavaScriptEnabled") + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + isCancelable = false + binding.captchaRefresh.setOnClickListener { + binding.captchaWebview.loadUrl(arguments?.getString(CAPTCHA_URL).orEmpty()) + } + binding.captchaClose.setOnClickListener { dismiss() } + + with(binding.captchaWebview) { + webView = this + with(settings) { + javaScriptEnabled = true + userAgentString = sdk.userAgent + } + + webViewClient = object : WebViewClient() { + override fun onPageFinished(view: WebView?, url: String?) { + view?.evaluateJavascript(CAPTCHA_CHECK_JS) { + if (it == "true") { + onChallengeAccepted() + } + } + } + } + + loadUrl(arguments?.getString(CAPTCHA_URL).orEmpty()) + } + } + + private fun onChallengeAccepted() { + runCatching { parentFragmentManager.setFragmentResult(CAPTCHA_SUCCESS, bundleOf()) } + .onFailure { Timber.e(it) } + showMessage(getString(R.string.captcha_verified_message)) + dismissAllowingStateLoss() + } + + override fun onDestroy() { + webView?.destroy() + super.onDestroy() + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferenceDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferenceDialog.kt index 477b762b9..c532377e1 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferenceDialog.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferenceDialog.kt @@ -1,19 +1,20 @@ package io.github.wulkanowy.ui.modules.conference +import android.app.Dialog import android.os.Bundle -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup +import androidx.core.os.bundleOf import androidx.core.view.isVisible -import androidx.fragment.app.DialogFragment +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.data.db.entities.Conference import io.github.wulkanowy.databinding.DialogConferenceBinding -import io.github.wulkanowy.utils.lifecycleAwareVariable +import io.github.wulkanowy.ui.base.BaseDialogFragment +import io.github.wulkanowy.utils.serializable import io.github.wulkanowy.utils.toFormattedString -class ConferenceDialog : DialogFragment() { - - private var binding: DialogConferenceBinding by lifecycleAwareVariable() +@AndroidEntryPoint +class ConferenceDialog : BaseDialogFragment() { private lateinit var conference: Conference @@ -22,23 +23,20 @@ class ConferenceDialog : DialogFragment() { private const val ARGUMENT_KEY = "item" fun newInstance(conference: Conference) = ConferenceDialog().apply { - arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, conference) } + arguments = bundleOf(ARGUMENT_KEY to conference) } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setStyle(STYLE_NO_TITLE, 0) - arguments?.let { - conference = it.getSerializable(ARGUMENT_KEY) as Conference - } + conference = requireArguments().serializable(ARGUMENT_KEY) } - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ) = DialogConferenceBinding.inflate(inflater).also { binding = it }.root + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return MaterialAlertDialogBuilder(requireContext(), theme) + .setView(DialogConferenceBinding.inflate(layoutInflater).apply { binding = this }.root) + .create() + } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -57,4 +55,4 @@ class ConferenceDialog : DialogFragment() { conferenceDialogAgendaTitle.isVisible = conference.agenda.isNotBlank() } } -} \ No newline at end of file +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferenceFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferenceFragment.kt index b9642b1c7..0cd3150c7 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferenceFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferenceFragment.kt @@ -16,7 +16,7 @@ import javax.inject.Inject @AndroidEntryPoint class ConferenceFragment : BaseFragment(R.layout.fragment_conference), - ConferenceView, MainView.TitledView { + ConferenceView, MainView.TitledView, MainView.MainChildView { @Inject lateinit var presenter: ConferencePresenter @@ -109,6 +109,14 @@ class ConferenceFragment : BaseFragment(R.layout.frag (activity as? MainActivity)?.showDialogFragment(ConferenceDialog.newInstance(conference)) } + override fun onFragmentReselected() { + if (::presenter.isInitialized) presenter.onFragmentReselected() + } + + override fun resetView() { + binding.conferenceRecycler.smoothScrollToPosition(0) + } + override fun onDestroyView() { presenter.onDetachView() super.onDestroyView() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferencePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferencePresenter.kt index f53648930..1178c7200 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 @@ -96,4 +96,11 @@ class ConferencePresenter @Inject constructor( .onResourceError(errorHandler::dispatch) .launch() } + + fun onFragmentReselected() { + Timber.i("Conference is reselected") + if (view?.isViewEmpty == false) { + view?.resetView() + } + } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferenceView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferenceView.kt index 4f73394df..3299a1f08 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferenceView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferenceView.kt @@ -28,4 +28,6 @@ interface ConferenceView : BaseView { fun showContent(show: Boolean) fun openConferenceDialog(conference: Conference) + + fun resetView() } 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 de0b4a6c9..b7a0796c5 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 @@ -11,14 +11,17 @@ import androidx.recyclerview.widget.DefaultItemAnimator import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.databinding.FragmentDashboardBinding import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.modules.account.accountdetails.AccountDetailsFragment import io.github.wulkanowy.ui.modules.attendance.summary.AttendanceSummaryFragment +import io.github.wulkanowy.ui.modules.captcha.CaptchaDialog.Companion.CAPTCHA_SUCCESS import io.github.wulkanowy.ui.modules.conference.ConferenceFragment import io.github.wulkanowy.ui.modules.dashboard.adapters.DashboardAdapter +import io.github.wulkanowy.ui.modules.dashboard.viewholders.AdminMessageViewHolder import io.github.wulkanowy.ui.modules.exam.ExamFragment import io.github.wulkanowy.ui.modules.grade.GradeFragment import io.github.wulkanowy.ui.modules.homework.HomeworkFragment @@ -29,7 +32,12 @@ import io.github.wulkanowy.ui.modules.message.MessageFragment import io.github.wulkanowy.ui.modules.notificationscenter.NotificationsCenterFragment import io.github.wulkanowy.ui.modules.schoolannouncement.SchoolAnnouncementFragment import io.github.wulkanowy.ui.modules.timetable.TimetableFragment -import io.github.wulkanowy.utils.* +import io.github.wulkanowy.utils.capitalise +import io.github.wulkanowy.utils.dpToPx +import io.github.wulkanowy.utils.getErrorString +import io.github.wulkanowy.utils.getThemeAttrColor +import io.github.wulkanowy.utils.openInternetBrowser +import io.github.wulkanowy.utils.toFormattedString import java.time.LocalDate import javax.inject.Inject @@ -56,11 +64,15 @@ class DashboardFragment : BaseFragment(R.layout.fragme return ((recyclerWidth - margin) / resources.displayMetrics.density).toInt() } + override val isViewEmpty + get() = dashboardAdapter.itemCount == 0 + companion object { fun newInstance() = DashboardFragment() } + @Suppress("DEPRECATION") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setHasOptionsMenu(true) @@ -70,6 +82,13 @@ class DashboardFragment : BaseFragment(R.layout.fragme super.onViewCreated(view, savedInstanceState) binding = FragmentDashboardBinding.bind(view) presenter.onAttachView(this) + initializeCaptchaResultObserver() + } + + private fun initializeCaptchaResultObserver() { + childFragmentManager.setFragmentResultListener(CAPTCHA_SUCCESS, this) { _, _ -> + presenter.onRetryAfterCaptcha() + } } override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { @@ -147,7 +166,7 @@ class DashboardFragment : BaseFragment(R.layout.fragme val values = requireContext().resources.getStringArray(R.array.dashboard_tile_values) val selectedItemsState = values.map { value -> selectedItems.any { it.name == value } } - AlertDialog.Builder(requireContext()) + MaterialAlertDialogBuilder(requireContext()) .setTitle(R.string.pref_dashboard_appearance_tiles_title) .setMultiChoiceItems(entries, selectedItemsState.toBooleanArray()) { _, _, _ -> } .setPositiveButton(android.R.string.ok) { dialog, _ -> @@ -180,8 +199,17 @@ class DashboardFragment : BaseFragment(R.layout.fragme binding.dashboardRecycler.isVisible = show } - override fun showErrorView(show: Boolean) { + override fun showErrorView(show: Boolean, adminMessageItem: DashboardItem.AdminMessages?) { binding.dashboardErrorContainer.isVisible = show + binding.dashboardErrorAdminMessage.root.isVisible = adminMessageItem != null + + if (adminMessageItem != null) { + AdminMessageViewHolder( + binding = binding.dashboardErrorAdminMessage, + onAdminMessageDismissClickListener = presenter::onAdminMessageDismissed, + onAdminMessageClickListener = presenter::onAdminMessageSelected, + ).bind(adminMessageItem.adminMessage) + } } override fun setErrorDetails(error: Throwable) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardItem.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardItem.kt index e220ae236..d019dea68 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 @@ -33,18 +33,27 @@ sealed class DashboardItem(val type: Type) { } data class HorizontalGroup( - val unreadMessagesCount: Int? = null, - val attendancePercentage: Double? = null, - val luckyNumber: Int? = null, + val unreadMessagesCount: Cell? = null, + val attendancePercentage: Cell? = null, + val luckyNumber: Cell? = null, override val error: Throwable? = null, override val isLoading: Boolean = false ) : DashboardItem(Type.HORIZONTAL_GROUP) { + data class Cell( + val data: T?, + val error: Boolean, + val isLoading: Boolean, + ) { + val isHidden: Boolean + get() = data == null && !error && !isLoading + } + override val isDataLoaded - get() = unreadMessagesCount != null || attendancePercentage != null || luckyNumber != null + get() = unreadMessagesCount?.isLoading == false || attendancePercentage?.isLoading == false || luckyNumber?.isLoading == false val isFullDataLoaded - get() = luckyNumber != -1 && attendancePercentage != -1.0 && unreadMessagesCount != -1 + get() = luckyNumber?.isLoading != true && attendancePercentage?.isLoading != true && unreadMessagesCount?.isLoading != true } data class Grades( 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 9c15acc35..f033b5947 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 @@ -3,6 +3,7 @@ package io.github.wulkanowy.ui.modules.dashboard import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.RecyclerView import io.github.wulkanowy.ui.modules.dashboard.adapters.DashboardAdapter +import io.github.wulkanowy.ui.modules.dashboard.viewholders.AdminMessageViewHolder import java.util.* class DashboardItemMoveCallback( @@ -55,5 +56,5 @@ class DashboardItemMoveCallback( } private val RecyclerView.ViewHolder.isAdminMessageOrAccountItem: Boolean - get() = this is DashboardAdapter.AdminMessageViewHolder || this is DashboardAdapter.AccountViewHolder + get() = this is AdminMessageViewHolder || this is DashboardAdapter.AccountViewHolder } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt index 350300937..1e6f1c198 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,17 +1,48 @@ package io.github.wulkanowy.ui.modules.dashboard -import io.github.wulkanowy.data.* +import io.github.wulkanowy.data.Resource +import io.github.wulkanowy.data.dataOrNull import io.github.wulkanowy.data.db.entities.AdminMessage import io.github.wulkanowy.data.db.entities.LuckyNumber import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.enums.MessageFolder -import io.github.wulkanowy.data.repositories.* +import io.github.wulkanowy.data.enums.MessageType +import io.github.wulkanowy.data.errorOrNull +import io.github.wulkanowy.data.flatResourceFlow +import io.github.wulkanowy.data.mapResourceData +import io.github.wulkanowy.data.onResourceError +import io.github.wulkanowy.data.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.domain.adminmessage.GetAppropriateAdminMessageUseCase +import io.github.wulkanowy.domain.timetable.IsStudentHasLessonsOnWeekendUseCase import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.utils.AdsHelper import io.github.wulkanowy.utils.calculatePercentage import io.github.wulkanowy.utils.nextOrSameSchoolDay -import kotlinx.coroutines.flow.* +import io.github.wulkanowy.utils.sunday +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.filterNot +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import timber.log.Timber import java.time.Instant @@ -25,15 +56,15 @@ 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 isStudentHasLessonsOnWeekendUseCase: IsStudentHasLessonsOnWeekendUseCase, private val homeworkRepository: HomeworkRepository, private val examRepository: ExamRepository, private val conferenceRepository: ConferenceRepository, private val preferencesRepository: PreferencesRepository, private val schoolAnnouncementRepository: SchoolAnnouncementRepository, - private val adminMessageRepository: AdminMessageRepository, + private val getAppropriateAdminMessageUseCase: GetAppropriateAdminMessageUseCase, private val adsHelper: AdsHelper ) : BasePresenter(errorHandler, studentRepository) { @@ -47,6 +78,11 @@ class DashboardPresenter @Inject constructor( private val firstLoadedItemList = mutableListOf() + private val selectedDashboardTiles + get() = preferencesRepository.selectedDashboardTiles + .filterNot { it == DashboardItem.Tile.ADS && !adsHelper.canShowAd } + .toSet() + private lateinit var lastError: Throwable override fun onAttachView(view: DashboardView) { @@ -58,10 +94,19 @@ class DashboardPresenter @Inject constructor( showContent(false) } + val selectedDashboardTilesFlow = preferencesRepository.selectedDashboardTilesFlow + .map { selectedDashboardTiles } + val isAdsEnabledFlow = preferencesRepository.isAdsEnabledFlow + .filter { (adsHelper.canShowAd && it) || !it } + .map { selectedDashboardTiles } + val isMobileAdsSdkInitializedFlow = adsHelper.isMobileAdsSdkInitialized + .filter { it } + .map { selectedDashboardTiles } + merge( - preferencesRepository.selectedDashboardTilesFlow, - preferencesRepository.isAdsEnabledFlow - .map { preferencesRepository.selectedDashboardTiles } + selectedDashboardTilesFlow, + isAdsEnabledFlow, + isMobileAdsSdkInitializedFlow ) .onEach { loadData(tilesToLoad = it) } .launch("dashboard_pref") @@ -70,7 +115,7 @@ class DashboardPresenter @Inject constructor( fun onAdminMessageDismissed(adminMessage: AdminMessage) { preferencesRepository.dismissedAdminMessageIds += adminMessage.id - loadData(preferencesRepository.selectedDashboardTiles) + loadData(selectedDashboardTiles) } fun onDragAndDropEnd(list: List) { @@ -160,19 +205,23 @@ class DashboardPresenter @Inject constructor( DashboardItem.Type.ACCOUNT -> { updateData(DashboardItem.Account(student), forceRefresh) } + DashboardItem.Type.HORIZONTAL_GROUP -> { loadHorizontalGroup(student, forceRefresh) } + DashboardItem.Type.LESSONS -> loadLessons(student, forceRefresh) DashboardItem.Type.GRADES -> loadGrades(student, forceRefresh) DashboardItem.Type.HOMEWORK -> loadHomework(student, forceRefresh) DashboardItem.Type.ANNOUNCEMENTS -> { loadSchoolAnnouncements(student, forceRefresh) } + DashboardItem.Type.EXAMS -> loadExams(student, forceRefresh) DashboardItem.Type.CONFERENCES -> { loadConferences(student, forceRefresh) } + DashboardItem.Type.ADS -> loadAds(forceRefresh) DashboardItem.Type.ADMIN_MESSAGE -> loadAdminMessage(student, forceRefresh) } @@ -182,7 +231,7 @@ class DashboardPresenter @Inject constructor( fun onSwipeRefresh() { Timber.i("Force refreshing the dashboard") - loadData(preferencesRepository.selectedDashboardTiles, forceRefresh = true) + loadData(selectedDashboardTiles, forceRefresh = true) } fun onRetry() { @@ -190,7 +239,15 @@ class DashboardPresenter @Inject constructor( showErrorView(false) showProgress(true) } - loadData(preferencesRepository.selectedDashboardTiles, forceRefresh = true) + loadData(selectedDashboardTiles, forceRefresh = true) + } + + fun onRetryAfterCaptcha() { + view?.run { + showErrorView(false) + showProgress(true) + } + loadData(selectedDashboardTiles, forceRefresh = true) } fun onViewReselected() { @@ -211,7 +268,7 @@ class DashboardPresenter @Inject constructor( } fun onDashboardTileSettingsSelected(): Boolean { - view?.showDashboardTileSettings(preferencesRepository.selectedDashboardTiles.toList()) + view?.showDashboardTileSettings(selectedDashboardTiles.toList()) return true } @@ -227,65 +284,83 @@ 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 selectedTiles = selectedDashboardTiles val flowSuccess = flowOf(Resource.Success(null)) + val luckyNumberFlow = luckyNumberRepository.getLuckyNumber(student, forceRefresh) .mapResourceData { it ?: LuckyNumber(0, LocalDate.now(), 0) } + .onResourceError { errorHandler.dispatch(it) } .takeIf { DashboardItem.Tile.LUCKY_NUMBER in selectedTiles } ?: flowSuccess - val messageFLow = messageRepository.getMessages( - student = student, - mailbox = mailbox, - folder = MessageFolder.RECEIVED, - forceRefresh = forceRefresh - ).takeIf { DashboardItem.Tile.MESSAGES in selectedTiles } ?: flowSuccess + val messageFLow = flatResourceFlow { + val mailbox = messageRepository.getMailboxByStudent(student) - val attendanceFlow = attendanceSummaryRepository.getAttendanceSummary( - student = student, - semester = semester, - subjectId = -1, - forceRefresh = forceRefresh - ).takeIf { DashboardItem.Tile.ATTENDANCE in selectedTiles } ?: flowSuccess + messageRepository.getMessages( + student = student, + mailbox = mailbox, + folder = MessageFolder.RECEIVED, + forceRefresh = forceRefresh + ) + } + .onResourceError { errorHandler.dispatch(it) } + .takeIf { DashboardItem.Tile.MESSAGES in selectedTiles } ?: flowSuccess + + val attendanceFlow = flatResourceFlow { + val semester = semesterRepository.getCurrentSemester(student) + attendanceSummaryRepository.getAttendanceSummary( + student = student, + semester = semester, + subjectId = -1, + forceRefresh = forceRefresh + ) + } + .onResourceError { errorHandler.dispatch(it) } + .takeIf { DashboardItem.Tile.ATTENDANCE in selectedTiles } ?: flowSuccess emitAll( combine( - luckyNumberFlow, - messageFLow, - attendanceFlow + flow = luckyNumberFlow, + flow2 = messageFLow, + flow3 = attendanceFlow, ) { luckyNumberResource, messageResource, attendanceResource -> val resList = listOf(luckyNumberResource, messageResource, attendanceResource) - resList.firstNotNullOfOrNull { it.errorOrNull }?.let { throw it } - val isLoading = resList.any { it is Resource.Loading } - val luckyNumber = luckyNumberResource.dataOrNull?.luckyNumber - val messageCount = messageResource.dataOrNull?.count { it.unread } - val attendancePercentage = attendanceResource.dataOrNull?.calculatePercentage() - - DashboardItem.HorizontalGroup( - isLoading = isLoading, - attendancePercentage = if (attendancePercentage == 0.0 && isLoading) -1.0 else attendancePercentage, - unreadMessagesCount = if (messageCount == 0 && isLoading) -1 else messageCount, - luckyNumber = if (luckyNumber == 0 && isLoading) -1 else luckyNumber + resList to DashboardItem.HorizontalGroup( + isLoading = resList.any { it is Resource.Loading }, + error = resList.map { it.errorOrNull }.let { errors -> + if (errors.all { it != null }) { + errors.firstOrNull() + } else null + }, + attendancePercentage = DashboardItem.HorizontalGroup.Cell( + data = attendanceResource.dataOrNull?.calculatePercentage(), + error = attendanceResource.errorOrNull != null, + isLoading = attendanceResource is Resource.Loading, + ), + unreadMessagesCount = DashboardItem.HorizontalGroup.Cell( + data = messageResource.dataOrNull?.count { it.unread }, + error = messageResource.errorOrNull != null, + isLoading = messageResource is Resource.Loading, + ), + luckyNumber = DashboardItem.HorizontalGroup.Cell( + data = luckyNumberResource.dataOrNull?.luckyNumber, + error = luckyNumberResource.errorOrNull != null, + isLoading = luckyNumberResource is Resource.Loading, + ) ) }) } - .filterNot { it.isLoading && forceRefresh } + .filterNot { (_, it) -> it.isLoading && forceRefresh } .distinctUntilChanged() - .onEach { + .onEach { (_, it) -> updateData(it, forceRefresh) if (it.isLoading) { Timber.i("Loading horizontal group data started") - - if (it.isFullDataLoaded) { - firstLoadedItemList += DashboardItem.Type.HORIZONTAL_GROUP - } } else { + firstLoadedItemList += DashboardItem.Type.HORIZONTAL_GROUP Timber.i("Loading horizontal group result: Success") } } @@ -297,7 +372,7 @@ class DashboardPresenter @Inject constructor( ) errorHandler.dispatch(it) } - .launch("horizontal_group ${if (forceRefresh) "-forceRefresh" else ""}") + .launchWithUniqueRefreshJob("horizontal_group", forceRefresh) } private fun loadGrades(student: Student, forceRefresh: Boolean) { @@ -331,13 +406,14 @@ class DashboardPresenter @Inject constructor( subjectWithGrades = it.dataOrNull, gradeTheme = preferencesRepository.gradeColorTheme, isLoading = true - ), forceRefresh + ), false ) if (!it.dataOrNull.isNullOrEmpty()) { firstLoadedItemList += DashboardItem.Type.GRADES } } + is Resource.Success -> { Timber.i("Loading dashboard grades result: Success") updateData( @@ -348,6 +424,7 @@ class DashboardPresenter @Inject constructor( forceRefresh ) } + is Resource.Error -> { Timber.i("Loading dashboard grades result: An exception occurred") errorHandler.dispatch(it.error) @@ -361,14 +438,17 @@ class DashboardPresenter @Inject constructor( private fun loadLessons(student: Student, forceRefresh: Boolean) { flatResourceFlow { val semester = semesterRepository.getCurrentSemester(student) - val date = LocalDate.now().nextOrSameSchoolDay + val date = when (isStudentHasLessonsOnWeekendUseCase(student, semester)) { + true -> LocalDate.now() + else -> LocalDate.now().nextOrSameSchoolDay + } timetableRepository.getTimetable( student = student, semester = semester, start = date, - end = date.plusDays(1), - forceRefresh = forceRefresh + end = date.sunday, + forceRefresh = forceRefresh, ) } .onEach { @@ -378,19 +458,21 @@ class DashboardPresenter @Inject constructor( if (forceRefresh) return@onEach updateData( DashboardItem.Lessons(it.dataOrNull, isLoading = true), - forceRefresh + false ) 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) @@ -433,17 +515,19 @@ class DashboardPresenter @Inject constructor( val data = it.dataOrNull.orEmpty() updateData( DashboardItem.Homework(data, isLoading = true), - forceRefresh + false ) 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) @@ -465,17 +549,19 @@ class DashboardPresenter @Inject constructor( if (forceRefresh) return@onEach updateData( DashboardItem.Announcements(it.dataOrNull.orEmpty(), isLoading = true), - forceRefresh + false ) 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) @@ -506,17 +592,19 @@ class DashboardPresenter @Inject constructor( if (forceRefresh) return@onEach updateData( DashboardItem.Exams(it.dataOrNull.orEmpty(), isLoading = true), - forceRefresh + false ) if (!it.dataOrNull.isNullOrEmpty()) { firstLoadedItemList += DashboardItem.Type.EXAMS } } + is Resource.Success -> { Timber.i("Loading dashboard exams result: Success") updateData(DashboardItem.Exams(it.data), forceRefresh) } + is Resource.Error -> { Timber.i("Loading dashboard exams result: An exception occurred") errorHandler.dispatch(it.error) @@ -545,17 +633,19 @@ class DashboardPresenter @Inject constructor( if (forceRefresh) return@onEach updateData( DashboardItem.Conferences(it.dataOrNull.orEmpty(), isLoading = true), - forceRefresh + false ) 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) @@ -567,19 +657,20 @@ class DashboardPresenter @Inject constructor( } private fun loadAdminMessage(student: Student, forceRefresh: Boolean) { - flatResourceFlow { adminMessageRepository.getAdminMessages(student) } - .filter { - val data = it.dataOrNull ?: return@filter true - val isDismissed = data.id in preferencesRepository.dismissedAdminMessageIds - !isDismissed - } + flatResourceFlow { + getAppropriateAdminMessageUseCase( + student = student, + type = MessageType.DASHBOARD_MESSAGE, + ) + } .onEach { when (it) { is Resource.Loading -> { Timber.i("Loading dashboard admin message data started") if (forceRefresh) return@onEach - updateData(DashboardItem.AdminMessages(), forceRefresh) + updateData(DashboardItem.AdminMessages(), false) } + is Resource.Success -> { Timber.i("Loading dashboard admin message result: Success") updateData( @@ -587,9 +678,10 @@ class DashboardPresenter @Inject constructor( forceRefresh = forceRefresh ) } + is Resource.Error -> { Timber.i("Loading dashboard admin message result: An exception occurred") - errorHandler.dispatch(it.error) + Timber.e(it.error) updateData( dashboardItem = DashboardItem.AdminMessages( adminMessage = null, @@ -606,7 +698,7 @@ class DashboardPresenter @Inject constructor( private fun loadAds(forceRefresh: Boolean) { presenterScope.launch { if (!forceRefresh) { - updateData(DashboardItem.Ads(), forceRefresh) + updateData(DashboardItem.Ads(), false) } val dashboardAdItem = @@ -727,11 +819,13 @@ class DashboardPresenter @Inject constructor( val filteredItems = itemsLoadedList.filterNot { it.type == DashboardItem.Type.ACCOUNT || it.type == DashboardItem.Type.ADMIN_MESSAGE } + val dataLoadedAdminMessageItem = + itemsLoadedList.find { it.type == DashboardItem.Type.ADMIN_MESSAGE && it.isDataLoaded } as DashboardItem.AdminMessages? val isAccountItemError = itemsLoadedList.find { it.type == DashboardItem.Type.ACCOUNT }?.error != null val isGeneralError = filteredItems.none { it.error == null } && filteredItems.isNotEmpty() || isAccountItemError - val firstError = itemsLoadedList.mapNotNull { it.error }.firstOrNull() + val firstError = itemsLoadedList.firstNotNullOfOrNull { it.error } val filteredOriginalLoadedList = dashboardItemLoadedList.filterNot { it.type == DashboardItem.Type.ACCOUNT } @@ -748,7 +842,7 @@ class DashboardPresenter @Inject constructor( showRefresh(false) if ((forceRefresh && wasGeneralError) || !forceRefresh) { showContent(false) - showErrorView(true) + showErrorView(true, dataLoadedAdminMessageItem) setErrorDetails(lastError) } } @@ -776,6 +870,28 @@ class DashboardPresenter @Inject constructor( onEach { if (it is Resource.Success) { cancelJobs(jobName) + } else if (it is Resource.Error) { + cancelJobs(jobName) + } + }.launch(jobName) + } else { + launch(jobName) + } + } + + @JvmName("launchWithUniqueRefreshJobHorizontalGroup") + private fun Flow>, *>>.launchWithUniqueRefreshJob( + name: String, + forceRefresh: Boolean + ) { + val jobName = if (forceRefresh) "$name-forceRefresh" else name + + if (forceRefresh) { + onEach { (resources, _) -> + if (resources.all { it is Resource.Success<*> }) { + cancelJobs(jobName) + } else if (resources.any { it is Resource.Error<*> }) { + cancelJobs(jobName) } }.launch(jobName) } else { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardView.kt index 767885434..56a0a773a 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardView.kt @@ -6,6 +6,8 @@ interface DashboardView : BaseView { val tileWidth: Int + val isViewEmpty: Boolean + fun initView() fun updateData(data: List) @@ -18,7 +20,7 @@ interface DashboardView : BaseView { fun showRefresh(show: Boolean) - fun showErrorView(show: Boolean) + fun showErrorView(show: Boolean, adminMessageItem: DashboardItem.AdminMessages? = null) fun setErrorDetails(error: Throwable) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/adapters/DashboardAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/adapters/DashboardAdapter.kt index a3c423a8b..7c74cae80 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/adapters/DashboardAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/adapters/DashboardAdapter.kt @@ -1,8 +1,6 @@ package io.github.wulkanowy.ui.modules.dashboard.adapters import android.annotation.SuppressLint -import android.content.res.ColorStateList -import android.graphics.Color import android.graphics.Typeface import android.os.Handler import android.os.Looper @@ -24,6 +22,7 @@ import io.github.wulkanowy.data.db.entities.TimetableHeader import io.github.wulkanowy.data.enums.GradeColorTheme import io.github.wulkanowy.databinding.* import io.github.wulkanowy.ui.modules.dashboard.DashboardItem +import io.github.wulkanowy.ui.modules.dashboard.viewholders.AdminMessageViewHolder import io.github.wulkanowy.utils.* import timber.log.Timber import java.time.Duration @@ -109,7 +108,9 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter AdminMessageViewHolder( - ItemDashboardAdminMessageBinding.inflate(inflater, parent, false) + ItemDashboardAdminMessageBinding.inflate(inflater, parent, false), + onAdminMessageDismissClickListener = onAdminMessageDismissClickListener, + onAdminMessageClickListener = onAdminMessageClickListener, ) DashboardItem.Type.ADS.ordinal -> AdsViewHolder( ItemDashboardAdsBinding.inflate(inflater, parent, false) @@ -128,7 +129,7 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter bindAnnouncementsViewHolder(holder, position) is ExamsViewHolder -> bindExamsViewHolder(holder, position) is ConferencesViewHolder -> bindConferencesViewHolder(holder, position) - is AdminMessageViewHolder -> bindAdminMessage(holder, position) + is AdminMessageViewHolder -> holder.bind((items[position] as DashboardItem.AdminMessages).adminMessage) is AdsViewHolder -> bindAdsViewHolder(holder, position) } } @@ -171,81 +172,105 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter { + updateMarginsRelative( + end = if (isAttendanceHidden && isMessagesHidden && !isLuckyNumberHidden) { + 0 + } else context.dpToPx(8f).toInt() + ) + } + } + } + + private fun ItemDashboardHorizontalGroupBinding.bindMessages( + item: DashboardItem.HorizontalGroup, + isWideErrorShow: Boolean + ) { + dashboardHorizontalGroupItemMessageError.isVisible = item.unreadMessagesCount?.error == true + with(dashboardHorizontalGroupItemMessageValue) { + isVisible = item.unreadMessagesCount?.error != true + text = item.unreadMessagesCount?.data.toString() + } + with(dashboardHorizontalGroupItemMessageContainer) { + isVisible = item.unreadMessagesCount?.isHidden == false && !isWideErrorShow + setOnClickListener { onMessageTileClickListener() } + } + } + + private fun ItemDashboardHorizontalGroupBinding.bindAttendance( + item: DashboardItem.HorizontalGroup, + isWideErrorShow: Boolean + ) { + val attendancePercentage = item.attendancePercentage?.data val attendanceColor = when { attendancePercentage == null || attendancePercentage == .0 -> { - context.getThemeAttrColor(R.attr.colorOnSurface) + root.context.getThemeAttrColor(R.attr.colorOnSurface) } attendancePercentage <= ATTENDANCE_SECOND_WARNING_THRESHOLD -> { - context.getThemeAttrColor(R.attr.colorPrimary) + root.context.getThemeAttrColor(R.attr.colorPrimary) } attendancePercentage <= ATTENDANCE_FIRST_WARNING_THRESHOLD -> { - context.getThemeAttrColor(R.attr.colorTimetableChange) + root.context.getThemeAttrColor(R.attr.colorTimetableChange) } - else -> context.getThemeAttrColor(R.attr.colorOnSurface) + else -> root.context.getThemeAttrColor(R.attr.colorOnSurface) } val attendanceString = if (attendancePercentage == null || attendancePercentage == .0) { - context.getString(R.string.dashboard_horizontal_group_no_data) + root.context.getString(R.string.dashboard_horizontal_group_no_data) } else { "%.2f%%".format(attendancePercentage) } - with(binding.dashboardHorizontalGroupItemAttendanceValue) { + dashboardHorizontalGroupItemAttendanceError.isVisible = + item.attendancePercentage?.error == true + with(dashboardHorizontalGroupItemAttendanceValue) { + isVisible = item.attendancePercentage?.error != true text = attendanceString setTextColor(attendanceColor) } - - with(binding) { - dashboardHorizontalGroupItemMessageValue.text = unreadMessagesCount.toString() - dashboardHorizontalGroupItemLuckyValue.text = if (luckyNumber == 0) { - context.getString(R.string.dashboard_horizontal_group_no_data) - } else luckyNumber?.toString() - - dashboardHorizontalGroupItemInfoContainer.isVisible = error != null || isLoadingVisible - dashboardHorizontalGroupItemInfoProgress.isVisible = isLoadingVisible - dashboardHorizontalGroupItemInfoErrorText.isVisible = error != null - - with(dashboardHorizontalGroupItemLuckyContainer) { - isVisible = luckyNumber != null && luckyNumber != -1 && !isLoadingVisible - setOnClickListener { onLuckyNumberTileClickListener() } - - updateLayoutParams { - updateMarginsRelative( - end = if (attendancePercentage == null && unreadMessagesCount == null && luckyNumber != null) { - 0 - } else { - context.dpToPx(8f).toInt() - } - ) + with(dashboardHorizontalGroupItemAttendanceContainer) { + isVisible = item.attendancePercentage?.isHidden == false && !isWideErrorShow + setOnClickListener { onAttendanceTileClickListener() } + updateLayoutParams { + matchConstraintPercentWidth = when { + item.luckyNumber?.isHidden == true && item.unreadMessagesCount?.isHidden == true -> 1.0f + item.luckyNumber?.isHidden == true || item.unreadMessagesCount?.isHidden == true -> 0.5f + else -> 0.4f } } - - with(dashboardHorizontalGroupItemAttendanceContainer) { - isVisible = - attendancePercentage != null && attendancePercentage != -1.0 && !isLoadingVisible - updateLayoutParams { - matchConstraintPercentWidth = when { - luckyNumber == null && unreadMessagesCount == null -> 1.0f - luckyNumber == null || unreadMessagesCount == null -> 0.5f - else -> 0.4f - } - } - setOnClickListener { onAttendanceTileClickListener() } - } - - with(dashboardHorizontalGroupItemMessageContainer) { - isVisible = - unreadMessagesCount != null && unreadMessagesCount != -1 && !isLoadingVisible - setOnClickListener { onMessageTileClickListener() } - } } } @@ -709,38 +734,6 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter { - context.getThemeAttrColor(R.attr.colorPrimary) to - context.getThemeAttrColor(R.attr.colorOnPrimary) - } - "MEDIUM" -> { - context.getThemeAttrColor(R.attr.colorMessageMedium) to Color.BLACK - } - else -> null to context.getThemeAttrColor(R.attr.colorOnSurface) - } - - with(adminMessageViewHolder.binding) { - dashboardAdminMessageItemTitle.text = item.title - dashboardAdminMessageItemTitle.setTextColor(textColor) - dashboardAdminMessageItemDescription.text = item.content - dashboardAdminMessageItemDescription.setTextColor(textColor) - dashboardAdminMessageItemIcon.setColorFilter(textColor) - dashboardAdminMessageItemDismiss.isVisible = item.isDismissible - dashboardAdminMessageItemDismiss.setOnClickListener { - onAdminMessageDismissClickListener(item) - } - - root.setCardBackgroundColor(backgroundColor?.let { ColorStateList.valueOf(it) }) - item.destinationUrl?.let { url -> - root.setOnClickListener { onAdminMessageClickListener(url) } - } - } - } - private fun bindAdsViewHolder(adsViewHolder: AdsViewHolder, position: Int) { val item = (items[position] as DashboardItem.Ads).adBanner ?: return val binding = adsViewHolder.binding @@ -794,9 +787,6 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter() { @@ -37,7 +39,9 @@ class DashboardGradesAdapter : RecyclerView.Adapter Unit, + private val onAdminMessageClickListener: (String?) -> Unit, +) : RecyclerView.ViewHolder(binding.root) { + + fun bind(item: AdminMessage?) { + item ?: return + + val context = binding.root.context + val (backgroundColor, textColor) = when (item.priority) { + "HIGH" -> { + context.getThemeAttrColor(R.attr.colorMessageHigh) to + context.getThemeAttrColor(R.attr.colorOnMessageHigh) + } + + "MEDIUM" -> { + context.getThemeAttrColor(R.attr.colorMessageMedium) to Color.BLACK + } + + else -> null to context.getThemeAttrColor(R.attr.colorOnSurface) + } + + with(binding) { + dashboardAdminMessageItemTitle.text = item.title + dashboardAdminMessageItemTitle.setTextColor(textColor) + dashboardAdminMessageItemDescription.text = item.content + dashboardAdminMessageItemDescription.setTextColor(textColor) + dashboardAdminMessageItemIcon.setColorFilter(textColor) + dashboardAdminMessageItemDismiss.isVisible = item.isOkVisible + dashboardAdminMessageItemClose.isVisible = item.isXVisible + dashboardAdminMessageItemDismiss.setTextColor(textColor) + dashboardAdminMessageItemClose.imageTintList = ColorStateList.valueOf(textColor) + dashboardAdminMessageItemDismiss.setOnClickListener { + onAdminMessageDismissClickListener(item) + } + dashboardAdminMessageItemClose.setOnClickListener { + onAdminMessageDismissClickListener(item) + } + + root.setCardBackgroundColor(backgroundColor?.let { ColorStateList.valueOf(it) }) + item.destinationUrl?.let { url -> + root.setOnClickListener { onAdminMessageClickListener(url) } + } + } + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/debug/DebugFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/DebugFragment.kt index 000916b17..9db01a307 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/debug/DebugFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/DebugFragment.kt @@ -2,6 +2,7 @@ package io.github.wulkanowy.ui.modules.debug import android.os.Bundle import android.view.View +import android.webkit.CookieManager import androidx.recyclerview.widget.LinearLayoutManager import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R @@ -58,6 +59,10 @@ class DebugFragment : BaseFragment(R.layout.fragment_debug (activity as? MainActivity)?.pushView(NotificationDebugFragment.newInstance()) } + override fun clearWebkitCookies() { + CookieManager.getInstance().removeAllCookies(null) + } + override fun onDestroyView() { presenter.onDetachView() super.onDestroyView() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/debug/DebugPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/DebugPresenter.kt index 67ac88861..816b59858 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/debug/DebugPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/DebugPresenter.kt @@ -15,6 +15,7 @@ class DebugPresenter @Inject constructor( val items = listOf( DebugItem(R.string.logviewer_title), DebugItem(R.string.notification_debug_title), + DebugItem(R.string.debug_cookies_clear), ) override fun onAttachView(view: DebugView) { @@ -31,6 +32,7 @@ class DebugPresenter @Inject constructor( when (item.title) { R.string.logviewer_title -> view?.openLogViewer() R.string.notification_debug_title -> view?.openNotificationsDebug() + R.string.debug_cookies_clear -> view?.clearWebkitCookies() else -> Timber.d("Unknown debug item: $item") } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/debug/DebugView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/DebugView.kt index 9396ec6ac..792d63d9e 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/debug/DebugView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/DebugView.kt @@ -11,4 +11,6 @@ interface DebugView : BaseView { fun openLogViewer() fun openNotificationsDebug() + + fun clearWebkitCookies() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/debug/logviewer/LogViewerFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/logviewer/LogViewerFragment.kt index 1e11c874b..929e72645 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/debug/logviewer/LogViewerFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/logviewer/LogViewerFragment.kt @@ -1,9 +1,7 @@ package io.github.wulkanowy.ui.modules.debug.logviewer import android.content.Intent -import android.content.Intent.EXTRA_EMAIL -import android.content.Intent.EXTRA_STREAM -import android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION +import android.content.Intent.* import android.os.Bundle import android.view.Menu import android.view.MenuInflater @@ -36,6 +34,7 @@ class LogViewerFragment : BaseFragment(R.layout.fragme fun newInstance() = LogViewerFragment() } + @Suppress("DEPRECATION") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setHasOptionsMenu(true) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/NotificationDebugPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/NotificationDebugPresenter.kt index d0dfcd696..cdd186b94 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/NotificationDebugPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/NotificationDebugPresenter.kt @@ -18,6 +18,7 @@ import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.ui.modules.debug.notification.mock.debugAttendanceItems import io.github.wulkanowy.ui.modules.debug.notification.mock.debugConferenceItems import io.github.wulkanowy.ui.modules.debug.notification.mock.debugExamItems +import io.github.wulkanowy.ui.modules.debug.notification.mock.debugGradeDescriptiveItems import io.github.wulkanowy.ui.modules.debug.notification.mock.debugGradeDetailsItems import io.github.wulkanowy.ui.modules.debug.notification.mock.debugGradeSummaryItems import io.github.wulkanowy.ui.modules.debug.notification.mock.debugHomeworkItems @@ -55,6 +56,14 @@ class NotificationDebugPresenter @Inject constructor( NotificationDebugItem(R.string.grade_summary_final_grade) { n -> withStudent { newGradeNotification.notifyFinal(debugGradeSummaryItems.take(n), it) } }, + NotificationDebugItem(R.string.grade_summary_descriptive) { n -> + withStudent { + newGradeNotification.notifyDescriptive( + debugGradeDescriptiveItems.take(n), + it + ) + } + }, NotificationDebugItem(R.string.homework_title) { n -> withStudent { newHomeworkNotification.notify(debugHomeworkItems.take(n), it) } }, diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/mock/gradeDescriptive.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/mock/gradeDescriptive.kt new file mode 100644 index 000000000..d5a0c089b --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/mock/gradeDescriptive.kt @@ -0,0 +1,48 @@ +package io.github.wulkanowy.ui.modules.debug.notification.mock + +import io.github.wulkanowy.data.db.entities.GradeDescriptive + +val debugGradeDescriptiveItems = listOf( + generateGradeDescriptive( + "Matematyka", + "Lorem ipsum dolor sit amet, consectetur adipiscing elit." + ), + generateGradeDescriptive("Fizyka", "Lorem ipsum dolor sit amet, consectetur adipiscing elit."), + generateGradeDescriptive( + "Geografia", + "Lorem ipsum dolor sit amet, consectetur adipiscing elit." + ), + generateGradeDescriptive( + "Sieci komputerowe", + "Lorem ipsum dolor sit amet, consectetur adipiscing elit." + ), + generateGradeDescriptive( + "Systemy operacyjne", + "Lorem ipsum dolor sit amet, consectetur adipiscing elit." + ), + generateGradeDescriptive( + "Język polski", + "Lorem ipsum dolor sit amet, consectetur adipiscing elit." + ), + generateGradeDescriptive( + "Język angielski", + "Lorem ipsum dolor sit amet, consectetur adipiscing elit." + ), + generateGradeDescriptive("Religia", "Lorem ipsum dolor sit amet, consectetur adipiscing elit."), + generateGradeDescriptive( + "Język niemiecki", + "Lorem ipsum dolor sit amet, consectetur adipiscing elit." + ), + generateGradeDescriptive( + "Wychowanie fizyczne", + "Lorem ipsum dolor sit amet, consectetur adipiscing elit." + ), +) + +private fun generateGradeDescriptive(subject: String, description: String) = + GradeDescriptive( + semesterId = 0, + studentId = 0, + subject = subject, + description = description + ) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/mock/message.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/mock/message.kt index 6ff26162b..27d8613a3 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 @@ -19,9 +19,12 @@ val debugMessageItems = listOf( private fun generateMessage(sender: String, subject: String) = Message( subject = subject, messageId = 123, + email = "", date = Instant.now(), folderId = 0, unread = true, + readBy = 2, + unreadBy = 2, hasAttachments = false, messageGlobalKey = "", correspondents = sender, 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 41adc008a..d452d74a5 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 @@ -1,21 +1,22 @@ package io.github.wulkanowy.ui.modules.exam +import android.app.Dialog import android.os.Bundle -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.DialogFragment +import androidx.core.os.bundleOf +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Exam import io.github.wulkanowy.databinding.DialogExamBinding -import io.github.wulkanowy.utils.lifecycleAwareVariable +import io.github.wulkanowy.ui.base.BaseDialogFragment import io.github.wulkanowy.utils.openCalendarEventAdd +import io.github.wulkanowy.utils.serializable import io.github.wulkanowy.utils.toFormattedString import java.time.LocalTime -class ExamDialog : DialogFragment() { - - private var binding: DialogExamBinding by lifecycleAwareVariable() +@AndroidEntryPoint +class ExamDialog : BaseDialogFragment() { private lateinit var exam: Exam @@ -24,23 +25,20 @@ class ExamDialog : DialogFragment() { private const val ARGUMENT_KEY = "Item" fun newInstance(exam: Exam) = ExamDialog().apply { - arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, exam) } + arguments = bundleOf(ARGUMENT_KEY to exam) } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setStyle(STYLE_NO_TITLE, 0) - arguments?.run { - exam = getSerializable(ARGUMENT_KEY) as Exam - } + exam = requireArguments().serializable(ARGUMENT_KEY) } - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ) = DialogExamBinding.inflate(inflater).apply { binding = this }.root + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return MaterialAlertDialogBuilder(requireContext(), theme) + .setView(DialogExamBinding.inflate(layoutInflater).apply { binding = this }.root) + .create() + } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamFragment.kt index ddd0e4a19..0123e2340 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamFragment.kt @@ -2,9 +2,7 @@ package io.github.wulkanowy.ui.modules.exam import android.os.Bundle import android.view.View -import android.view.View.GONE -import android.view.View.INVISIBLE -import android.view.View.VISIBLE +import android.view.View.* import androidx.recyclerview.widget.LinearLayoutManager import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R @@ -20,7 +18,7 @@ import javax.inject.Inject @AndroidEntryPoint class ExamFragment : BaseFragment(R.layout.fragment_exam), ExamView, - MainView.TitledView { + MainView.TitledView, MainView.MainChildView { @Inject lateinit var presenter: ExamPresenter @@ -64,7 +62,7 @@ class ExamFragment : BaseFragment(R.layout.fragment_exam), examPreviousButton.setOnClickListener { presenter.onPreviousWeek() } examNextButton.setOnClickListener { presenter.onNextWeek() } - examNavContainer.elevation = requireContext().dpToPx(8f) + examNavContainer.elevation = requireContext().dpToPx(3f) } } @@ -126,6 +124,14 @@ class ExamFragment : BaseFragment(R.layout.fragment_exam), (activity as? MainActivity)?.showDialogFragment(ExamDialog.newInstance(exam)) } + override fun onFragmentReselected() { + if (::presenter.isInitialized) presenter.onViewReselected() + } + + override fun resetView() { + binding.examRecycler.smoothScrollToPosition(0) + } + override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) outState.putLong(SAVED_DATE_KEY, presenter.currentDate.toEpochDay()) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamPresenter.kt index 99b0bcb87..858140728 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 @@ -175,4 +175,17 @@ class ExamPresenter @Inject constructor( ) } } + + fun onViewReselected() { + Timber.i("Exam view is reselected") + + baseDate = now().nextOrSameSchoolDay + + if (currentDate != baseDate) { + reloadView(baseDate) + loadData() + } else if (view?.isViewEmpty == false) { + view?.resetView() + } + } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamView.kt index 45b9e788c..677fac40e 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamView.kt @@ -34,4 +34,6 @@ interface ExamView : BaseView { fun showPreButton(show: Boolean) fun showExamDialog(exam: Exam) + + fun resetView() } 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 b6733d4f2..e8a5fa254 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,81 +1,113 @@ package io.github.wulkanowy.ui.modules.grade -import io.github.wulkanowy.data.* +import io.github.wulkanowy.data.Resource +import io.github.wulkanowy.data.dataOrNull import io.github.wulkanowy.data.db.entities.Grade +import io.github.wulkanowy.data.db.entities.GradeDescriptive import io.github.wulkanowy.data.db.entities.GradeSummary import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student +import io.github.wulkanowy.data.errorOrNull +import io.github.wulkanowy.data.flatResourceFlow +import io.github.wulkanowy.data.mapData +import io.github.wulkanowy.data.mapResourceData import io.github.wulkanowy.data.repositories.GradeRepository import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.SemesterRepository import io.github.wulkanowy.sdk.Sdk -import io.github.wulkanowy.ui.modules.grade.GradeAverageMode.* +import io.github.wulkanowy.ui.modules.grade.GradeAverageMode.ALL_YEAR +import io.github.wulkanowy.ui.modules.grade.GradeAverageMode.BOTH_SEMESTERS +import io.github.wulkanowy.ui.modules.grade.GradeAverageMode.ONE_SEMESTER import io.github.wulkanowy.utils.calcAverage import io.github.wulkanowy.utils.changeModifier -import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest import javax.inject.Inject -@OptIn(FlowPreview::class) +@OptIn(ExperimentalCoroutinesApi::class) class GradeAverageProvider @Inject constructor( private val semesterRepository: SemesterRepository, private val gradeRepository: GradeRepository, private val preferencesRepository: PreferencesRepository ) { - private val plusModifier get() = preferencesRepository.gradePlusModifier + private data class AverageCalcParams( + val gradeAverageMode: GradeAverageMode, + val forceAverageCalc: Boolean, + val isOptionalArithmeticAverage: Boolean, + val plusModifier: Double, + val minusModifier: Double, + ) - private val minusModifier get() = preferencesRepository.gradeMinusModifier - - private val isOptionalArithmeticAverage get() = preferencesRepository.isOptionalArithmeticAverage - - fun getGradesDetailsWithAverage(student: Student, semesterId: Int, forceRefresh: Boolean) = + fun getGradesDetailsWithAverage( + student: Student, + semesterId: Int, + forceRefresh: Boolean + ): Flow>> = combine( + flow = preferencesRepository.gradeAverageModeFlow, + flow2 = preferencesRepository.gradeAverageForceCalcFlow, + flow3 = preferencesRepository.isOptionalArithmeticAverageFlow, + flow4 = preferencesRepository.gradePlusModifierFlow, + flow5 = preferencesRepository.gradeMinusModifierFlow, + ) { gradeAverageMode, forceAverageCalc, isOptionalArithmeticAverage, plusModifier, minusModifier -> + AverageCalcParams( + gradeAverageMode = gradeAverageMode, + forceAverageCalc = forceAverageCalc, + isOptionalArithmeticAverage = isOptionalArithmeticAverage, + plusModifier = plusModifier, + minusModifier = minusModifier, + ) + }.flatMapLatest { params -> flatResourceFlow { val semesters = semesterRepository.getSemesters(student) - - when (preferencesRepository.gradeAverageMode) { + when (params.gradeAverageMode) { ONE_SEMESTER -> getGradeSubjects( student = student, - semester = semesters.single { it.semesterId == semesterId }, - forceRefresh = forceRefresh + semester = semesters.first { it.semesterId == semesterId }, + forceRefresh = forceRefresh, + params = params, ) + BOTH_SEMESTERS -> calculateCombinedAverage( student = student, semesters = semesters, semesterId = semesterId, forceRefresh = forceRefresh, - averageMode = BOTH_SEMESTERS + config = params, ) + ALL_YEAR -> calculateCombinedAverage( student = student, semesters = semesters, semesterId = semesterId, forceRefresh = forceRefresh, - averageMode = ALL_YEAR + config = params, ) } - }.distinctUntilChanged() + } + }.distinctUntilChanged() private fun calculateCombinedAverage( student: Student, semesters: List, semesterId: Int, forceRefresh: Boolean, - averageMode: GradeAverageMode + config: AverageCalcParams, ): Flow>> { - val isGradeAverageForceCalc = preferencesRepository.gradeAverageForceCalc val selectedSemester = semesters.single { it.semesterId == semesterId } val firstSemester = semesters.single { it.diaryId == selectedSemester.diaryId && it.semesterName == 1 } val selectedSemesterGradeSubjects = - getGradeSubjects(student, selectedSemester, forceRefresh) + getGradeSubjects(student, selectedSemester, forceRefresh, config) if (selectedSemester == firstSemester) return selectedSemesterGradeSubjects - val firstSemesterGradeSubjects = getGradeSubjects(student, firstSemester, forceRefresh) + val firstSemesterGradeSubjects = + getGradeSubjects(student, firstSemester, forceRefresh, config) return selectedSemesterGradeSubjects.combine(firstSemesterGradeSubjects) { secondSemesterGradeSubject, firstSemesterGradeSubject -> if (firstSemesterGradeSubject.errorOrNull != null) { @@ -91,21 +123,21 @@ class GradeAverageProvider @Inject constructor( val firstSemesterSubject = firstSemesterGradeSubject.dataOrNull.orEmpty() .singleOrNull { it.subject == secondSemesterSubject.subject } - val updatedAverage = if (averageMode == ALL_YEAR) { + val updatedAverage = if (config.gradeAverageMode == ALL_YEAR) { calculateAllYearAverage( student = student, isAnyVulcanAverage = isAnyVulcanAverageInFirstSemester || isAnyVulcanAverageInSecondSemester, - isGradeAverageForceCalc = isGradeAverageForceCalc, secondSemesterSubject = secondSemesterSubject, - firstSemesterSubject = firstSemesterSubject + firstSemesterSubject = firstSemesterSubject, + config = config, ) } else { calculateBothSemestersAverage( student = student, isAnyVulcanAverage = isAnyVulcanAverageInFirstSemester || isAnyVulcanAverageInSecondSemester, - isGradeAverageForceCalc = isGradeAverageForceCalc, secondSemesterSubject = secondSemesterSubject, - firstSemesterSubject = firstSemesterSubject + firstSemesterSubject = firstSemesterSubject, + config = config ) } secondSemesterSubject.copy(average = updatedAverage) @@ -117,17 +149,17 @@ class GradeAverageProvider @Inject constructor( private fun calculateAllYearAverage( student: Student, isAnyVulcanAverage: Boolean, - isGradeAverageForceCalc: Boolean, secondSemesterSubject: GradeSubject, - firstSemesterSubject: GradeSubject? - ) = if (!isAnyVulcanAverage || isGradeAverageForceCalc) { - val updatedSecondSemesterGrades = - secondSemesterSubject.grades.updateModifiers(student) - val updatedFirstSemesterGrades = - firstSemesterSubject?.grades?.updateModifiers(student).orEmpty() + firstSemesterSubject: GradeSubject?, + config: AverageCalcParams, + ) = if (!isAnyVulcanAverage || config.forceAverageCalc) { + val updatedSecondSemesterGrades = secondSemesterSubject.grades + .updateModifiers(student, config) + val updatedFirstSemesterGrades = firstSemesterSubject?.grades + ?.updateModifiers(student, config).orEmpty() (updatedSecondSemesterGrades + updatedFirstSemesterGrades).calcAverage( - isOptionalArithmeticAverage + config.isOptionalArithmeticAverage ) } else { secondSemesterSubject.average @@ -136,66 +168,109 @@ class GradeAverageProvider @Inject constructor( private fun calculateBothSemestersAverage( student: Student, isAnyVulcanAverage: Boolean, - isGradeAverageForceCalc: Boolean, secondSemesterSubject: GradeSubject, - firstSemesterSubject: GradeSubject? - ): Double = if (!isAnyVulcanAverage || isGradeAverageForceCalc) { - val divider = if (secondSemesterSubject.grades.any { it.weightValue > .0 }) 2 else 1 + firstSemesterSubject: GradeSubject?, + config: AverageCalcParams, + ): Double { + return if (!isAnyVulcanAverage || config.forceAverageCalc) { + val divider = if (secondSemesterSubject.grades.any { it.weightValue > .0 }) 2 else 1 + val secondSemesterAverage = secondSemesterSubject.grades + .updateModifiers(student, config) + .calcAverage(config.isOptionalArithmeticAverage) + val firstSemesterAverage = firstSemesterSubject?.grades + ?.updateModifiers(student, config) + ?.calcAverage(config.isOptionalArithmeticAverage) ?: secondSemesterAverage - val secondSemesterAverage = secondSemesterSubject.grades.updateModifiers(student) - .calcAverage(isOptionalArithmeticAverage) - val firstSemesterAverage = firstSemesterSubject?.grades?.updateModifiers(student) - ?.calcAverage(isOptionalArithmeticAverage) ?: secondSemesterAverage + (secondSemesterAverage + firstSemesterAverage) / divider + } else { + val divider = if (secondSemesterSubject.average > 0) 2 else 1 - (secondSemesterAverage + firstSemesterAverage) / divider - } else { - val divider = if (secondSemesterSubject.average > 0) 2 else 1 - - (secondSemesterSubject.average + (firstSemesterSubject?.average - ?: secondSemesterSubject.average)) / divider + secondSemesterSubject.average.plus( + (firstSemesterSubject?.average ?: secondSemesterSubject.average) + ) / divider + } } private fun getGradeSubjects( student: Student, semester: Semester, - forceRefresh: Boolean + forceRefresh: Boolean, + params: AverageCalcParams, ): Flow>> { - val isGradeAverageForceCalc = preferencesRepository.gradeAverageForceCalc - return gradeRepository.getGrades(student, semester, forceRefresh = forceRefresh) .mapResourceData { res -> - val (details, summaries) = res + val (details, summaries, descriptives) = res val isAnyAverage = summaries.any { it.average != .0 } val allGrades = details.groupBy { it.subject } + val descriptiveGradesBySubject = descriptives.associateBy { it.subject } - val items = summaries.emulateEmptySummaries( - student = student, - semester = semester, - grades = allGrades.toList(), - calcAverage = isAnyAverage - ).map { summary -> - val grades = allGrades[summary.subject].orEmpty() - GradeSubject( - subject = summary.subject, - average = if (!isAnyAverage || isGradeAverageForceCalc) { - grades.updateModifiers(student).calcAverage(isOptionalArithmeticAverage) - } else summary.average, - points = summary.pointsSum, - summary = summary, - grades = grades, - isVulcanAverage = isAnyAverage + val items = summaries + .createEmptySummariesByGradesIfNeeded( + student = student, + semester = semester, + grades = allGrades.toList(), + calcAverage = isAnyAverage, + params = params, ) - } + .createEmptySummariesByDescriptiveGradesIfNeeded( + student = student, + semester = semester, + descriptives = descriptives, + ) + .map { summary -> + val grades = allGrades[summary.subject].orEmpty() + val descriptiveGrade = descriptiveGradesBySubject[summary.subject] + + GradeSubject( + subject = summary.subject, + average = if (!isAnyAverage || params.forceAverageCalc) { + grades.updateModifiers(student, params) + .calcAverage(params.isOptionalArithmeticAverage) + } else summary.average, + points = summary.pointsSum, + summary = summary, + grades = grades, + descriptive = descriptiveGrade, + isVulcanAverage = isAnyAverage + ) + } items } } - private fun List.emulateEmptySummaries( + private fun List.createEmptySummariesByDescriptiveGradesIfNeeded( + student: Student, + semester: Semester, + descriptives: List + ): List { + val summarySubjects = this.map { it.subject } + val gradeSummaryToAdd = descriptives.mapNotNull { gradeDescriptive -> + if (gradeDescriptive.subject in summarySubjects) return@mapNotNull null + + GradeSummary( + studentId = student.studentId, + semesterId = semester.semesterId, + position = 0, + subject = gradeDescriptive.subject, + predictedGrade = "", + finalGrade = "", + proposedPoints = "", + finalPoints = "", + pointsSum = "", + average = .0 + ) + } + + return this + gradeSummaryToAdd + } + + private fun List.createEmptySummariesByGradesIfNeeded( student: Student, semester: Semester, grades: List>>, - calcAverage: Boolean + calcAverage: Boolean, + params: AverageCalcParams, ): List { if (isNotEmpty() && size > grades.size) return this @@ -211,15 +286,16 @@ class GradeAverageProvider @Inject constructor( proposedPoints = "", finalPoints = "", pointsSum = "", - average = if (calcAverage) details.updateModifiers(student) - .calcAverage(isOptionalArithmeticAverage) else .0 + average = if (calcAverage) details.updateModifiers(student, params) + .calcAverage(params.isOptionalArithmeticAverage) else .0 ) } } - private fun List.updateModifiers(student: Student): List { - return if (student.loginMode == Sdk.Mode.SCRAPPER.name) { - map { it.changeModifier(plusModifier, minusModifier) } - } else this - } + private fun List.updateModifiers( + student: Student, + params: AverageCalcParams, + ): List = if (student.loginMode == Sdk.Mode.SCRAPPER.name) { + map { it.changeModifier(params.plusModifier, params.minusModifier) } + } else this } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeFragment.kt index 0a8561eec..7ce07eb68 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeFragment.kt @@ -8,6 +8,7 @@ import android.view.View import android.view.View.INVISIBLE import android.view.View.VISIBLE import androidx.appcompat.app.AlertDialog +import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.tabs.TabLayoutMediator import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R @@ -51,6 +52,7 @@ class GradeFragment : BaseFragment(R.layout.fragment_grade override val currentPageIndex get() = binding.gradeViewPager.currentItem + @Suppress("DEPRECATION") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setHasOptionsMenu(true) @@ -140,7 +142,7 @@ class GradeFragment : BaseFragment(R.layout.fragment_grade val choices = semesters.map { getString(R.string.grade_semester, it.semesterName) } .toTypedArray() - AlertDialog.Builder(requireContext()) + MaterialAlertDialogBuilder(requireContext()) .setSingleChoiceItems(choices, selectedIndex) { dialog, which -> presenter.onSemesterSelected(which) dialog.dismiss() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeSubject.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeSubject.kt index 57be55ee3..a465551f9 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeSubject.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeSubject.kt @@ -1,6 +1,7 @@ package io.github.wulkanowy.ui.modules.grade import io.github.wulkanowy.data.db.entities.Grade +import io.github.wulkanowy.data.db.entities.GradeDescriptive import io.github.wulkanowy.data.db.entities.GradeSummary data class GradeSubject( @@ -8,6 +9,7 @@ data class GradeSubject( val average: Double, val points: String, val summary: GradeSummary, + val descriptive: GradeDescriptive?, val grades: List, val isVulcanAverage: Boolean ) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsAdapter.kt index e5c3bb63e..15b5db031 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsAdapter.kt @@ -1,6 +1,7 @@ package io.github.wulkanowy.ui.modules.grade.details import android.annotation.SuppressLint +import android.content.res.ColorStateList import android.content.res.Resources import android.view.LayoutInflater import android.view.View @@ -17,9 +18,10 @@ import io.github.wulkanowy.databinding.HeaderGradeDetailsBinding import io.github.wulkanowy.databinding.ItemGradeDetailsBinding import io.github.wulkanowy.ui.base.BaseExpandableAdapter import io.github.wulkanowy.utils.getBackgroundColor +import io.github.wulkanowy.utils.getCompatColor import io.github.wulkanowy.utils.toFormattedString import timber.log.Timber -import java.util.BitSet +import java.util.* import javax.inject.Inject class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter() { @@ -203,7 +205,9 @@ class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter grade.description 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 34594111f..39f72f8bc 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 @@ -1,21 +1,23 @@ package io.github.wulkanowy.ui.modules.grade.details +import android.app.Dialog +import android.content.res.ColorStateList import android.os.Bundle -import android.view.LayoutInflater import android.view.View import android.view.View.GONE -import android.view.ViewGroup -import androidx.fragment.app.DialogFragment +import androidx.core.content.ContextCompat +import androidx.core.os.bundleOf +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Grade import io.github.wulkanowy.data.enums.GradeColorTheme import io.github.wulkanowy.databinding.DialogGradeBinding +import io.github.wulkanowy.ui.base.BaseDialogFragment import io.github.wulkanowy.utils.* - -class GradeDetailsDialog : DialogFragment() { - - private var binding: DialogGradeBinding by lifecycleAwareVariable() +@AndroidEntryPoint +class GradeDetailsDialog : BaseDialogFragment() { private lateinit var grade: Grade @@ -27,29 +29,25 @@ class GradeDetailsDialog : DialogFragment() { private const val COLOR_THEME_KEY = "Theme" - fun newInstance(grade: Grade, colorTheme: GradeColorTheme) = - GradeDetailsDialog().apply { - arguments = Bundle().apply { - putSerializable(ARGUMENT_KEY, grade) - putSerializable(COLOR_THEME_KEY, colorTheme) - } - } + fun newInstance(grade: Grade, colorTheme: GradeColorTheme) = GradeDetailsDialog().apply { + arguments = bundleOf( + ARGUMENT_KEY to grade, + COLOR_THEME_KEY to colorTheme + ) + } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setStyle(STYLE_NO_TITLE, 0) - arguments?.run { - grade = getSerializable(ARGUMENT_KEY) as Grade - gradeColorTheme = getSerializable(COLOR_THEME_KEY) as GradeColorTheme - } + grade = requireArguments().serializable(ARGUMENT_KEY) + gradeColorTheme = requireArguments().serializable(COLOR_THEME_KEY) } - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ) = DialogGradeBinding.inflate(inflater).apply { binding = this }.root + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return MaterialAlertDialogBuilder(requireContext(), theme) + .setView(DialogGradeBinding.inflate(layoutInflater).apply { binding = this }.root) + .create() + } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -57,10 +55,9 @@ class GradeDetailsDialog : DialogFragment() { with(binding) { gradeDialogSubject.text = grade.subject - gradeDialogColorAndWeightValue.run { - text = context.getString(R.string.grade_weight_value, grade.weight) - setBackgroundResource(grade.getGradeColor()) - } + gradeDialogWeightValue.text = grade.weight + gradeDialogWeightLayout.backgroundTintList = + ColorStateList.valueOf(requireContext().getCompatColor(grade.getGradeColor())) gradeDialogDateValue.text = grade.date.toFormattedString() gradeDialogColorValue.text = getString(grade.colorStringId) @@ -74,7 +71,12 @@ class GradeDetailsDialog : DialogFragment() { gradeDialogValue.run { text = grade.entry - setBackgroundResource(grade.getBackgroundColor(gradeColorTheme)) + backgroundTintList = ColorStateList.valueOf( + ContextCompat.getColor( + requireContext(), + grade.getBackgroundColor(gradeColorTheme) + ) + ) } gradeDialogTeacherValue.text = grade.teacher.ifBlank { getString(R.string.all_no_data) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsFragment.kt index 81f3226ad..23d767a6f 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsFragment.kt @@ -5,9 +5,7 @@ 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 androidx.recyclerview.widget.LinearLayoutManager import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R @@ -42,6 +40,7 @@ class GradeDetailsFragment : override val isViewEmpty get() = gradeDetailsAdapter.itemCount == 0 + @Suppress("DEPRECATION") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setHasOptionsMenu(true) 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 4261c507d..d9621f51e 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsPresenter.kt @@ -1,13 +1,22 @@ package io.github.wulkanowy.ui.modules.grade.details -import io.github.wulkanowy.data.* import io.github.wulkanowy.data.db.entities.Grade import io.github.wulkanowy.data.enums.GradeExpandMode -import io.github.wulkanowy.data.enums.GradeSortingMode.* +import io.github.wulkanowy.data.enums.GradeSortingMode.ALPHABETIC +import io.github.wulkanowy.data.enums.GradeSortingMode.AVERAGE +import io.github.wulkanowy.data.enums.GradeSortingMode.DATE +import io.github.wulkanowy.data.flatResourceFlow +import io.github.wulkanowy.data.logResourceStatus +import io.github.wulkanowy.data.onResourceData +import io.github.wulkanowy.data.onResourceError +import io.github.wulkanowy.data.onResourceIntermediate +import io.github.wulkanowy.data.onResourceNotLoading +import io.github.wulkanowy.data.onResourceSuccess import io.github.wulkanowy.data.repositories.GradeRepository import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.SemesterRepository import io.github.wulkanowy.data.repositories.StudentRepository +import io.github.wulkanowy.data.resourceFlow import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.ui.modules.grade.GradeAverageProvider @@ -207,20 +216,20 @@ class GradeDetailsPresenter @Inject constructor( AVERAGE -> gradeSubjects.sortedByDescending { it.average } } } - .map { (subject, average, points, _, grades) -> - val subItems = grades + .map { gradeSubject -> + val subItems = gradeSubject.grades .sortedByDescending { it.date } .map { GradeDetailsItem(it, ViewType.ITEM) } val gradeDetailsItems = listOf( GradeDetailsItem( GradeDetailsHeader( - subject = subject, - average = average, - pointsSum = points, + subject = gradeSubject.subject, + average = gradeSubject.average, + pointsSum = gradeSubject.points, grades = subItems ).apply { - newGrades = grades.filter { grade -> !grade.isRead }.size + newGrades = gradeSubject.grades.filter { grade -> !grade.isRead }.size }, ViewType.HEADER ) ) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/statistics/GradeStatisticsAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/statistics/GradeStatisticsAdapter.kt index fd0ac5471..e5f1eba0c 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 @@ -22,6 +22,8 @@ import io.github.wulkanowy.databinding.ItemGradeStatisticsHeaderBinding import io.github.wulkanowy.databinding.ItemGradeStatisticsPieBinding import io.github.wulkanowy.utils.getThemeAttrColor import javax.inject.Inject +import kotlin.math.max +import kotlin.math.roundToInt class GradeStatisticsAdapter @Inject constructor() : RecyclerView.Adapter() { @@ -116,7 +118,9 @@ class GradeStatisticsAdapter @Inject constructor() : } ) - binding.gradeStatisticsTypeSwitch.setOnCheckedChangeListener { _, checkedId -> + binding.gradeStatisticsTypeSwitch.addOnButtonCheckedListener { _, checkedId, isChecked -> + if (!isChecked) return@addOnButtonCheckedListener + currentDataType = when (checkedId) { R.id.gradeStatisticsTypePartial -> GradeStatisticsItem.DataType.PARTIAL R.id.gradeStatisticsTypeSemester -> GradeStatisticsItem.DataType.SEMESTER @@ -267,7 +271,7 @@ class GradeStatisticsAdapter @Inject constructor() : valueTextSize = 12f valueTextColor = binding.root.context.getThemeAttrColor(android.R.attr.textColorPrimary) valueFormatter = object : ValueFormatter() { - override fun getBarLabel(barEntry: BarEntry) = "${barEntry.y}%" + override fun getBarLabel(barEntry: BarEntry) = "${barEntry.y}" } colors = gradePointsColors } @@ -302,15 +306,20 @@ class GradeStatisticsAdapter @Inject constructor() : } xAxis.setDrawLabels(false) xAxis.setDrawGridLines(false) + + val yMaxFromValues = (max(points.others, points.student)).roundToInt() + 30f + val yMaxFromValuesWithMargin = ((yMaxFromValues / 10.0).roundToInt() * 10).toFloat() + val yMax = yMaxFromValuesWithMargin.coerceAtLeast(100f) + val yLabelCount = (yMax / 10).toInt() + 1 with(axisLeft) { axisMinimum = 0f - axisMaximum = 100f - labelCount = 11 + axisMaximum = yMax + labelCount = yLabelCount } with(axisRight) { axisMinimum = 0f - axisMaximum = 100f - labelCount = 11 + axisMaximum = yMax + labelCount = yLabelCount } invalidate() } 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 2af59c011..edc384c5e 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 @@ -15,6 +15,7 @@ import io.github.wulkanowy.ui.modules.grade.GradeFragment import io.github.wulkanowy.ui.modules.grade.GradeView import io.github.wulkanowy.utils.dpToPx import io.github.wulkanowy.utils.getThemeAttrColor +import io.github.wulkanowy.utils.serializable import io.github.wulkanowy.utils.setOnItemSelectedListener import javax.inject.Inject @@ -48,8 +49,8 @@ class GradeStatisticsFragment : messageContainer = binding.gradeStatisticsRecycler presenter.onAttachView( view = this, - type = savedInstanceState?.getSerializable(SAVED_CHART_TYPE) as? GradeStatisticsItem.DataType, - subjectName = savedInstanceState?.getSerializable(SAVED_SUBJECT_NAME) as? String, + type = savedInstanceState?.serializable(SAVED_CHART_TYPE), + subjectName = savedInstanceState?.serializable(SAVED_SUBJECT_NAME), ) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryAdapter.kt index 082c847e5..95cf97bed 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryAdapter.kt @@ -2,15 +2,16 @@ package io.github.wulkanowy.ui.modules.grade.summary import android.annotation.SuppressLint import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup +import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView import io.github.wulkanowy.R -import io.github.wulkanowy.data.db.entities.GradeSummary import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.databinding.ItemGradeSummaryBinding import io.github.wulkanowy.databinding.ScrollableHeaderGradeSummaryBinding +import io.github.wulkanowy.sdk.scrapper.grades.isGradeValid import io.github.wulkanowy.utils.calcFinalAverage +import io.github.wulkanowy.utils.ifNullOrBlank import java.util.Locale import javax.inject.Inject @@ -23,7 +24,7 @@ class GradeSummaryAdapter @Inject constructor( ITEM(2) } - var items = emptyList() + var items = emptyList() var onCalculatedHelpClickListener: () -> Unit = {} @@ -43,9 +44,11 @@ class GradeSummaryAdapter @Inject constructor( ViewType.HEADER.id -> HeaderViewHolder( ScrollableHeaderGradeSummaryBinding.inflate(inflater, parent, false) ) + ViewType.ITEM.id -> ItemViewHolder( ItemGradeSummaryBinding.inflate(inflater, parent, false) ) + else -> throw IllegalStateException() } } @@ -59,19 +62,23 @@ class GradeSummaryAdapter @Inject constructor( private fun bindHeaderViewHolder(binding: ScrollableHeaderGradeSummaryBinding) { if (items.isEmpty()) return + val gradeSummaries = items + .filter { it.gradeDescriptive == null } + .map { it.gradeSummary } val context = binding.root.context - val finalItemsCount = items.count { it.finalGrade.matches("[0-6][+-]?".toRegex()) } - val calculatedItemsCount = items.count { value -> value.average != 0.0 } - val allItemsCount = items.count { !it.subject.equals("zachowanie", true) } - val finalAverage = items.calcFinalAverage( + val finalItemsCount = gradeSummaries.count { isGradeValid(it.finalGrade) } + val calculatedItemsCount = gradeSummaries.count { value -> value.average != 0.0 } + val allItemsCount = gradeSummaries.count { !it.subject.equals("zachowanie", true) } + val finalAverage = gradeSummaries.calcFinalAverage( preferencesRepository.gradePlusModifier, preferencesRepository.gradeMinusModifier ) - val calculatedAverage = items.filter { value -> value.average != 0.0 } + val calculatedAverage = gradeSummaries.filter { value -> value.average != 0.0 } .map { values -> values.average } .reversed() // fix average precision .average() + .let { if (it.isNaN()) 0.0 else it } with(binding) { gradeSummaryScrollableHeaderFinal.text = formatAverage(finalAverage) @@ -94,16 +101,28 @@ class GradeSummaryAdapter @Inject constructor( } @SuppressLint("SetTextI18n") - private fun bindItemViewHolder(binding: ItemGradeSummaryBinding, item: GradeSummary) { - with(binding) { - gradeSummaryItemTitle.text = item.subject - gradeSummaryItemPoints.text = item.pointsSum - gradeSummaryItemAverage.text = formatAverage(item.average, "") - gradeSummaryItemPredicted.text = "${item.predictedGrade} ${item.proposedPoints}".trim() - gradeSummaryItemFinal.text = "${item.finalGrade} ${item.finalPoints}".trim() + private fun bindItemViewHolder(binding: ItemGradeSummaryBinding, item: GradeSummaryItem) { + val (gradeSummary, gradeDescriptive) = item - gradeSummaryItemPointsContainer.visibility = - if (item.pointsSum.isBlank()) View.GONE else View.VISIBLE + with(binding) { + gradeSummaryItemTitle.text = gradeSummary.subject + gradeSummaryItemPoints.text = gradeSummary.pointsSum + gradeSummaryItemAverage.text = formatAverage(gradeSummary.average, "") + gradeSummaryItemPredicted.text = + "${gradeSummary.predictedGrade} ${gradeSummary.proposedPoints}".trim() + gradeSummaryItemFinal.text = + "${gradeSummary.finalGrade} ${gradeSummary.finalPoints}".trim() + gradeSummaryItemDescriptive.text = gradeDescriptive?.description.ifNullOrBlank { + root.context.getString(R.string.all_no_data) + } + + gradeSummaryItemFinalDivider.isVisible = gradeDescriptive == null + gradeSummaryItemPredictedDivider.isVisible = gradeDescriptive == null + gradeSummaryItemPointsDivider.isVisible = gradeDescriptive == null + gradeSummaryItemPredictedContainer.isVisible = gradeDescriptive == null + gradeSummaryItemFinalContainer.isVisible = gradeDescriptive == null + gradeSummaryItemDescriptiveContainer.isVisible = gradeDescriptive != null + gradeSummaryItemPointsContainer.isVisible = gradeSummary.pointsSum.isNotBlank() } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryFragment.kt index 3810902ff..35b2edd58 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryFragment.kt @@ -5,11 +5,10 @@ import android.view.View import android.view.View.GONE import android.view.View.INVISIBLE import android.view.View.VISIBLE -import androidx.appcompat.app.AlertDialog import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R -import io.github.wulkanowy.data.db.entities.GradeSummary import io.github.wulkanowy.databinding.FragmentGradeSummaryBinding import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.modules.grade.GradeFragment @@ -71,7 +70,7 @@ class GradeSummaryFragment : } } - override fun updateData(data: List) { + override fun updateData(data: List) { with(gradeSummaryAdapter) { items = data notifyDataSetChanged() @@ -118,7 +117,7 @@ class GradeSummaryFragment : } override fun showCalculatedAverageHelpDialog() { - AlertDialog.Builder(requireContext()) + MaterialAlertDialogBuilder(requireContext()) .setTitle(R.string.grade_summary_calculated_average_help_dialog_title) .setMessage(R.string.grade_summary_calculated_average_help_dialog_message) .setPositiveButton(R.string.all_close) { _, _ -> } @@ -126,7 +125,7 @@ class GradeSummaryFragment : } override fun showFinalAverageHelpDialog() { - AlertDialog.Builder(requireContext()) + MaterialAlertDialogBuilder(requireContext()) .setTitle(R.string.grade_summary_final_average_help_dialog_title) .setMessage(R.string.grade_summary_final_average_help_dialog_message) .setPositiveButton(R.string.all_close) { _, _ -> } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryItem.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryItem.kt new file mode 100644 index 000000000..cf0f1d92e --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryItem.kt @@ -0,0 +1,9 @@ +package io.github.wulkanowy.ui.modules.grade.summary + +import io.github.wulkanowy.data.db.entities.GradeDescriptive +import io.github.wulkanowy.data.db.entities.GradeSummary + +data class GradeSummaryItem( + val gradeSummary: GradeSummary, + val gradeDescriptive: GradeDescriptive? +) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryPresenter.kt index 4d5a43d8f..d762df02b 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryPresenter.kt @@ -1,9 +1,16 @@ package io.github.wulkanowy.ui.modules.grade.summary -import io.github.wulkanowy.data.* -import io.github.wulkanowy.data.db.entities.GradeSummary -import io.github.wulkanowy.data.enums.GradeSortingMode -import io.github.wulkanowy.data.enums.GradeSortingMode.* +import io.github.wulkanowy.data.enums.GradeSortingMode.ALPHABETIC +import io.github.wulkanowy.data.enums.GradeSortingMode.AVERAGE +import io.github.wulkanowy.data.enums.GradeSortingMode.DATE +import io.github.wulkanowy.data.flatResourceFlow +import io.github.wulkanowy.data.logResourceStatus +import io.github.wulkanowy.data.mapResourceData +import io.github.wulkanowy.data.onResourceData +import io.github.wulkanowy.data.onResourceError +import io.github.wulkanowy.data.onResourceIntermediate +import io.github.wulkanowy.data.onResourceNotLoading +import io.github.wulkanowy.data.onResourceSuccess import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.ui.base.BasePresenter @@ -42,7 +49,7 @@ class GradeSummaryPresenter @Inject constructor( val student = studentRepository.getCurrentStudent() averageProvider.getGradesDetailsWithAverage(student, semesterId, forceRefresh) } - .logResourceStatus("load grade summary", showData = true) + .logResourceStatus("load grade summary") .mapResourceData { createGradeSummaryItems(it) } .onResourceData { view?.run { @@ -128,7 +135,7 @@ class GradeSummaryPresenter @Inject constructor( view?.showFinalAverageHelpDialog() } - private fun createGradeSummaryItems(items: List): List { + private fun createGradeSummaryItems(items: List): List { return items .filter { !checkEmpty(it) } .let { gradeSubjects -> @@ -136,21 +143,32 @@ class GradeSummaryPresenter @Inject constructor( DATE -> gradeSubjects.sortedByDescending { gradeDetailsWithAverage -> gradeDetailsWithAverage.grades.maxByOrNull { it.date }?.date } + ALPHABETIC -> gradeSubjects.sortedBy { gradeDetailsWithAverage -> gradeDetailsWithAverage.subject.lowercase() } + AVERAGE -> gradeSubjects.sortedByDescending { it.average } } } - .map { it.summary.copy(average = it.average) } + .map { + val gradeSummary = it.summary.copy(average = it.average) + val descriptive = it.descriptive + GradeSummaryItem( + gradeSummary = gradeSummary, + gradeDescriptive = descriptive, + ) + } + } private fun checkEmpty(gradeSummary: GradeSubject): Boolean { return gradeSummary.run { summary.finalGrade.isBlank() - && summary.predictedGrade.isBlank() - && average == .0 - && points.isBlank() + && summary.predictedGrade.isBlank() + && average == .0 + && points.isBlank() + && descriptive == null } } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryView.kt index 156731c31..36bd61421 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryView.kt @@ -1,6 +1,5 @@ package io.github.wulkanowy.ui.modules.grade.summary -import io.github.wulkanowy.data.db.entities.GradeSummary import io.github.wulkanowy.ui.base.BaseView interface GradeSummaryView : BaseView { @@ -13,7 +12,7 @@ interface GradeSummaryView : BaseView { fun initView() - fun updateData(data: List) + fun updateData(data: List) fun resetView() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkFragment.kt index d4eaade2c..0381acf35 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkFragment.kt @@ -21,7 +21,7 @@ import javax.inject.Inject @AndroidEntryPoint class HomeworkFragment : BaseFragment(R.layout.fragment_homework), - HomeworkView, MainView.TitledView { + HomeworkView, MainView.TitledView, MainView.MainChildView { @Inject lateinit var presenter: HomeworkPresenter @@ -67,7 +67,7 @@ class HomeworkFragment : BaseFragment(R.layout.fragment openAddHomeworkButton.setOnClickListener { presenter.onHomeworkAddButtonClicked() } - homeworkNavContainer.elevation = requireContext().dpToPx(8f) + homeworkNavContainer.elevation = requireContext().dpToPx(3f) } } @@ -133,6 +133,14 @@ class HomeworkFragment : BaseFragment(R.layout.fragment (activity as? MainActivity)?.showDialogFragment(HomeworkAddDialog()) } + override fun onFragmentReselected() { + if (::presenter.isInitialized) presenter.onViewReselected() + } + + override fun resetView() { + binding.homeworkRecycler.smoothScrollToPosition(0) + } + override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) outState.putLong(SAVED_DATE_KEY, presenter.currentDate.toEpochDay()) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkPresenter.kt index 2ac552b41..6b263e26d 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 @@ -177,8 +177,21 @@ class HomeworkPresenter @Inject constructor( showNextButton(!currentDate.plusDays(7).isHolidays) updateNavigationWeek( "${currentDate.monday.toFormattedString("dd.MM")} - " + - currentDate.sunday.toFormattedString("dd.MM") + currentDate.sunday.toFormattedString("dd.MM") ) } } + + fun onViewReselected() { + Timber.i("Homework view is reselected") + + baseDate = LocalDate.now().nextOrSameSchoolDay + + if (currentDate != baseDate) { + reloadView(baseDate) + loadData() + } else if (view?.isViewEmpty == false) { + view?.resetView() + } + } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkView.kt index 7c05ab865..56ba6c89e 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkView.kt @@ -36,4 +36,6 @@ interface HomeworkView : BaseView { fun showHomeworkDialog(homework: Homework) fun showAddHomeworkDialog() + + fun resetView() } 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 c2aff2b13..c51370ea7 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 @@ -1,10 +1,10 @@ package io.github.wulkanowy.ui.modules.homework.add +import android.app.Dialog 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.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.databinding.DialogHomeworkAddBinding @@ -21,20 +21,15 @@ class HomeworkAddDialog : BaseDialogFragment(), Homewo @Inject lateinit var presenter: HomeworkAddPresenter - // todo: move it to presenter + //todo: move it to presenter private var date: LocalDate? = null - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setStyle(STYLE_NO_TITLE, 0) + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return MaterialAlertDialogBuilder(requireContext(), theme) + .setView(DialogHomeworkAddBinding.inflate(layoutInflater).apply { binding = this }.root) + .create() } - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ) = DialogHomeworkAddBinding.inflate(inflater).apply { binding = this }.root - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) presenter.onAttachView(this) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsAdapter.kt index e03707a5c..1ad2a0e32 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsAdapter.kt @@ -31,14 +31,8 @@ class HomeworkDetailsAdapter @Inject constructor() : attachments = value?.attachments.orEmpty() } - var isHomeworkFullscreen = false - var onAttachmentClickListener: (url: String) -> Unit = {} - var onFullScreenClickListener = {} - - var onFullScreenExitClickListener = {} - var onDeleteClickListener: (homework: Homework) -> Unit = {} override fun getItemCount() = 1 + if (attachments.isNotEmpty()) attachments.size + 1 else 0 @@ -82,18 +76,6 @@ class HomeworkDetailsAdapter @Inject constructor() : homeworkDialogTeacher.text = homework?.teacher.ifNullOrBlank { noDataString } homeworkDialogContent.text = homework?.content.ifNullOrBlank { noDataString } homeworkDialogDelete.visibility = if (homework?.isAddedByUser == true) VISIBLE else GONE - homeworkDialogFullScreen.visibility = if (isHomeworkFullscreen) GONE else VISIBLE - homeworkDialogFullScreenExit.visibility = if (isHomeworkFullscreen) VISIBLE else GONE - homeworkDialogFullScreen.setOnClickListener { - homeworkDialogFullScreen.visibility = GONE - homeworkDialogFullScreenExit.visibility = VISIBLE - onFullScreenClickListener() - } - homeworkDialogFullScreenExit.setOnClickListener { - homeworkDialogFullScreen.visibility = VISIBLE - homeworkDialogFullScreenExit.visibility = GONE - onFullScreenExitClickListener() - } homeworkDialogDelete.setOnClickListener { onDeleteClickListener(homework!!) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsDialog.kt index f9d463510..1f9bc881b 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsDialog.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsDialog.kt @@ -1,19 +1,19 @@ package io.github.wulkanowy.ui.modules.homework.details import android.annotation.SuppressLint +import android.app.Dialog import android.os.Bundle -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup -import android.view.ViewGroup.LayoutParams.MATCH_PARENT -import android.view.ViewGroup.LayoutParams.WRAP_CONTENT +import androidx.core.os.bundleOf import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Homework import io.github.wulkanowy.databinding.DialogHomeworkBinding import io.github.wulkanowy.ui.base.BaseDialogFragment import io.github.wulkanowy.utils.openInternetBrowser +import io.github.wulkanowy.utils.serializable import javax.inject.Inject @AndroidEntryPoint @@ -35,23 +35,20 @@ class HomeworkDetailsDialog : BaseDialogFragment(), Homew private const val ARGUMENT_KEY = "Item" fun newInstance(homework: Homework) = HomeworkDetailsDialog().apply { - arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, homework) } + arguments = bundleOf(ARGUMENT_KEY to homework) } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setStyle(STYLE_NO_TITLE, 0) - arguments?.run { - homework = getSerializable(ARGUMENT_KEY) as Homework - } + homework = requireArguments().serializable(ARGUMENT_KEY) } - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ) = DialogHomeworkBinding.inflate(inflater).apply { binding = this }.root + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return MaterialAlertDialogBuilder(requireContext(), theme) + .setView(DialogHomeworkBinding.inflate(layoutInflater).apply { binding = this }.root) + .create() + } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -67,26 +64,11 @@ class HomeworkDetailsDialog : BaseDialogFragment(), Homew homeworkDialogClose.setOnClickListener { dismiss() } } - if (presenter.isHomeworkFullscreen) { - dialog?.window?.setLayout(MATCH_PARENT, MATCH_PARENT) - } else { - dialog?.window?.setLayout(WRAP_CONTENT, WRAP_CONTENT) - } - with(binding.homeworkDialogRecycler) { layoutManager = LinearLayoutManager(context) adapter = detailsAdapter.apply { onAttachmentClickListener = { context.openInternetBrowser(it, ::showMessage) } - onFullScreenClickListener = { - dialog?.window?.setLayout(MATCH_PARENT, MATCH_PARENT) - presenter.isHomeworkFullscreen = true - } - onFullScreenExitClickListener = { - dialog?.window?.setLayout(WRAP_CONTENT, WRAP_CONTENT) - presenter.isHomeworkFullscreen = false - } onDeleteClickListener = { homework -> presenter.deleteHomework(homework) } - isHomeworkFullscreen = presenter.isHomeworkFullscreen homework = this@HomeworkDetailsDialog.homework } } 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 e76df6bd0..84933f06b 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 @@ -5,7 +5,6 @@ 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 @@ -19,15 +18,8 @@ class HomeworkDetailsPresenter @Inject constructor( studentRepository: StudentRepository, private val homeworkRepository: HomeworkRepository, private val analytics: AnalyticsHelper, - private val preferencesRepository: PreferencesRepository ) : BasePresenter(errorHandler, studentRepository) { - var isHomeworkFullscreen - get() = preferencesRepository.isHomeworkFullscreen - set(value) { - preferencesRepository.isHomeworkFullscreen = value - } - override fun onAttachView(view: HomeworkDetailsView) { super.onAttachView(view) view.initView() 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 aac60b56d..88f295788 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 @@ -2,13 +2,17 @@ package io.github.wulkanowy.ui.modules.login import android.content.Context import android.content.Intent +import android.content.pm.PackageManager +import android.os.Build.VERSION_CODES.TIRAMISU import android.os.Bundle import android.view.MenuItem +import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager.POP_BACK_STACK_INCLUSIVE 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.data.pojos.RegisterUser import io.github.wulkanowy.databinding.ActivityLoginBinding import io.github.wulkanowy.ui.base.BaseActivity import io.github.wulkanowy.ui.modules.login.advanced.LoginAdvancedFragment @@ -16,7 +20,10 @@ 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.ui.modules.main.MainActivity +import io.github.wulkanowy.ui.modules.notifications.NotificationsFragment +import io.github.wulkanowy.utils.AppInfo +import io.github.wulkanowy.utils.InAppUpdateHelper import javax.inject.Inject @AndroidEntryPoint @@ -26,7 +33,10 @@ class LoginActivity : BaseActivity(), Logi override lateinit var presenter: LoginPresenter @Inject - lateinit var updateHelper: UpdateHelper + lateinit var inAppUpdateHelper: InAppUpdateHelper + + @Inject + lateinit var appInfo: AppInfo companion object { fun getStartIntent(context: Context) = Intent(context, LoginActivity::class.java) @@ -37,10 +47,10 @@ class LoginActivity : BaseActivity(), Logi setContentView(ActivityLoginBinding.inflate(layoutInflater).apply { binding = this }.root) setSupportActionBar(binding.loginToolbar) messageContainer = binding.loginContainer - updateHelper.messageContainer = binding.loginContainer + inAppUpdateHelper.messageContainer = binding.loginContainer presenter.onAttachView(this) - updateHelper.checkAndInstallUpdates(this) + inAppUpdateHelper.checkAndInstallUpdates() if (savedInstanceState == null) { openFragment(LoginFormFragment.newInstance(), clearBackStack = true) @@ -55,7 +65,7 @@ class LoginActivity : BaseActivity(), Logi } override fun onOptionsItemSelected(item: MenuItem): Boolean { - if (item.itemId == android.R.id.home) onBackPressed() + if (item.itemId == android.R.id.home) onBackPressedDispatcher.onBackPressed() return true } @@ -67,8 +77,24 @@ class LoginActivity : BaseActivity(), Logi openFragment(LoginSymbolFragment.newInstance(loginData)) } - fun navigateToStudentSelect(studentsWithSemesters: List) { - openFragment(LoginStudentSelectFragment.newInstance(studentsWithSemesters)) + fun navigateToStudentSelect(loginData: LoginData, registerUser: RegisterUser) { + openFragment(LoginStudentSelectFragment.newInstance(loginData, registerUser)) + } + + fun navigateToNotifications() { + val isNotificationsPermissionRequired = appInfo.systemVersion >= TIRAMISU + val isPermissionGranted = ContextCompat.checkSelfPermission( + this, "android.permission.POST_NOTIFICATIONS" + ) == PackageManager.PERMISSION_GRANTED + + if (isNotificationsPermissionRequired && !isPermissionGranted) { + openFragment(NotificationsFragment.newInstance(), clearBackStack = true) + } else navigateToFinish() + } + + fun navigateToFinish() { + startActivity(MainActivity.getStartIntent(this)) + finish() } fun onAdvancedLoginClick() { @@ -80,6 +106,8 @@ class LoginActivity : BaseActivity(), Logi } private fun openFragment(fragment: Fragment, clearBackStack: Boolean = false) { + supportFragmentManager.popBackStack(fragment::class.java.name, POP_BACK_STACK_INCLUSIVE) + supportFragmentManager.commit { replace(R.id.loginContainer, fragment) setReorderingAllowed(true) @@ -89,14 +117,6 @@ class LoginActivity : BaseActivity(), Logi 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) + inAppUpdateHelper.onResume() } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginData.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginData.kt index 5d4743589..b066cceb9 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginData.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginData.kt @@ -6,4 +6,7 @@ data class LoginData( val login: String, val password: String, val baseUrl: String, + val domainSuffix: String, + val defaultSymbol: String, + val userEnteredSymbol: String? = null, ) : 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 37ab71dce..4f709438a 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 @@ -4,13 +4,15 @@ import android.content.Context import android.database.sqlite.SQLiteConstraintException import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R -import io.github.wulkanowy.sdk.mobile.exception.InvalidPinException -import io.github.wulkanowy.sdk.mobile.exception.InvalidSymbolException -import io.github.wulkanowy.sdk.mobile.exception.InvalidTokenException -import io.github.wulkanowy.sdk.mobile.exception.TokenDeadException +import io.github.wulkanowy.sdk.hebe.exception.InvalidPinException +import io.github.wulkanowy.sdk.hebe.exception.InvalidTokenException +import io.github.wulkanowy.sdk.hebe.exception.TokenDeadException +import io.github.wulkanowy.sdk.hebe.exception.UnknownTokenException import io.github.wulkanowy.sdk.scrapper.login.BadCredentialsException import io.github.wulkanowy.ui.base.ErrorHandler import javax.inject.Inject +import io.github.wulkanowy.sdk.hebe.exception.InvalidSymbolException as InvalidHebeSymbolException +import io.github.wulkanowy.sdk.scrapper.login.InvalidSymbolException as InvalidScrapperSymbolException class LoginErrorHandler @Inject constructor( @ApplicationContext context: Context, @@ -32,9 +34,11 @@ class LoginErrorHandler @Inject constructor( is BadCredentialsException -> onBadCredentials(error.message) is SQLiteConstraintException -> onStudentDuplicate(resources.getString(R.string.login_duplicate_student)) is TokenDeadException -> onInvalidToken(resources.getString(R.string.login_expired_token)) + is UnknownTokenException, is InvalidTokenException -> onInvalidToken(resources.getString(R.string.login_invalid_token)) is InvalidPinException -> onInvalidPin(resources.getString(R.string.login_invalid_pin)) - is InvalidSymbolException -> onInvalidSymbol(resources.getString(R.string.login_invalid_symbol)) + is InvalidScrapperSymbolException, + is InvalidHebeSymbolException -> onInvalidSymbol(resources.getString(R.string.login_invalid_symbol)) else -> super.proceed(error) } } 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 37dcb38b3..13d2c14a7 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 @@ -5,10 +5,11 @@ import android.view.View import android.view.View.GONE import android.view.View.VISIBLE import android.widget.ArrayAdapter +import androidx.core.view.isVisible import androidx.core.widget.doOnTextChanged import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R -import io.github.wulkanowy.data.db.entities.StudentWithSemesters +import io.github.wulkanowy.data.pojos.RegisterUser import io.github.wulkanowy.databinding.FragmentLoginAdvancedBinding import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.ui.base.BaseFragment @@ -34,9 +35,9 @@ class LoginAdvancedFragment : override val formLoginType: String get() = when (binding.loginTypeSwitch.checkedRadioButtonId) { - R.id.loginTypeApi -> "API" - R.id.loginTypeScrapper -> "SCRAPPER" - else -> "HYBRID" + R.id.loginTypeApi -> Sdk.Mode.HEBE.name + R.id.loginTypeScrapper -> Sdk.Mode.SCRAPPER.name + else -> Sdk.Mode.HYBRID.name } override val formUsernameValue: String @@ -55,6 +56,9 @@ class LoginAdvancedFragment : get() = hostValues.getOrNull(hostKeys.indexOf(binding.loginFormHost.text.toString())) .orEmpty() + override val formDomainSuffix: String + get() = binding.loginFormDomainSuffix.text.toString().trim() + override val formHostSymbol: String get() = hostSymbols.getOrNull(hostKeys.indexOf(binding.loginFormHost.text.toString())) .orEmpty() @@ -99,7 +103,7 @@ class LoginAdvancedFragment : loginTypeSwitch.setOnCheckedChangeListener { _, checkedId -> presenter.onLoginModeSelected( when (checkedId) { - R.id.loginTypeApi -> Sdk.Mode.API + R.id.loginTypeApi -> Sdk.Mode.HEBE R.id.loginTypeScrapper -> Sdk.Mode.SCRAPPER else -> Sdk.Mode.HYBRID } @@ -279,6 +283,7 @@ class LoginAdvancedFragment : loginFormUsernameLayout.visibility = VISIBLE loginFormPassLayout.visibility = VISIBLE loginFormHostLayout.visibility = VISIBLE + loginFormDomainSuffixLayout.isVisible = true loginFormPinLayout.visibility = GONE loginFormSymbolLayout.visibility = VISIBLE loginFormTokenLayout.visibility = GONE @@ -290,6 +295,7 @@ class LoginAdvancedFragment : loginFormUsernameLayout.visibility = VISIBLE loginFormPassLayout.visibility = VISIBLE loginFormHostLayout.visibility = VISIBLE + loginFormDomainSuffixLayout.isVisible = true loginFormPinLayout.visibility = GONE loginFormSymbolLayout.visibility = VISIBLE loginFormTokenLayout.visibility = GONE @@ -301,6 +307,7 @@ class LoginAdvancedFragment : loginFormUsernameLayout.visibility = GONE loginFormPassLayout.visibility = GONE loginFormHostLayout.visibility = GONE + loginFormDomainSuffixLayout.isVisible = false loginFormPinLayout.visibility = VISIBLE loginFormSymbolLayout.visibility = VISIBLE loginFormTokenLayout.visibility = VISIBLE @@ -327,8 +334,8 @@ class LoginAdvancedFragment : (activity as? LoginActivity)?.navigateToSymbolFragment(loginData) } - override fun navigateToStudentSelect(studentsWithSemesters: List) { - (activity as? LoginActivity)?.navigateToStudentSelect(studentsWithSemesters) + override fun navigateToStudentSelect(loginData: LoginData, registerUser: RegisterUser) { + (activity as? LoginActivity)?.navigateToStudentSelect(loginData, registerUser) } 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 1b42c6c52..009f26e17 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,12 +1,13 @@ package io.github.wulkanowy.ui.modules.login.advanced 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.pojos.RegisterUser import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.resourceFlow import io.github.wulkanowy.sdk.Sdk +import io.github.wulkanowy.sdk.scrapper.getNormalizedSymbol import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.modules.login.LoginData import io.github.wulkanowy.ui.modules.login.LoginErrorHandler @@ -70,7 +71,7 @@ class LoginAdvancedPresenter @Inject constructor( fun updateUsernameLabel() { view?.apply { - setUsernameLabel(if ("vulcan" in formHostValue || "fakelog" in formHostValue) emailLabel else nicknameLabel) + setUsernameLabel(if ("vulcan" in formHostValue || "wulkanowy" in formHostValue) emailLabel else nicknameLabel) } } @@ -78,7 +79,7 @@ class LoginAdvancedPresenter @Inject constructor( view?.apply { clearPassError() clearUsernameError() - if (formHostValue.contains("fakelog")) { + if (formHostValue.contains("wulkanowy")) { setDefaultCredentials( "jan@fakelog.cf", "jan123", "powiatwulkanowy", "FK100000", "999999" ) @@ -91,14 +92,16 @@ class LoginAdvancedPresenter @Inject constructor( fun onLoginModeSelected(type: Sdk.Mode) { view?.run { when (type) { - Sdk.Mode.API -> { + Sdk.Mode.HEBE -> { showOnlyMobileApiModeInputs() showMobileApiWarningMessage() } + Sdk.Mode.SCRAPPER -> { showOnlyScrapperModeInputs() showScraperWarningMessage() } + Sdk.Mode.HYBRID -> { showOnlyHybridModeInputs() showHybridWarningMessage() @@ -139,23 +142,30 @@ class LoginAdvancedPresenter @Inject constructor( showProgress(true) showContent(false) } + is Resource.Success -> { analytics.logEvent( "registration_form", - "success" to true, - "students" to it.data.size, - "error" to "No error" - ) - val loginData = LoginData( - login = view?.formUsernameValue.orEmpty().trim(), - password = view?.formPassValue.orEmpty().trim(), - baseUrl = view?.formHostValue.orEmpty().trim() - ) - when (it.data.size) { - 0 -> view?.navigateToSymbol(loginData) - else -> view?.navigateToStudentSelect(it.data) - } + "success" to true, + "scrapperBaseUrl" to view?.formHostValue.orEmpty(), + "error" to "No error" + ) + val loginData = LoginData( + login = view?.formUsernameValue.orEmpty().trim(), + password = view?.formPassValue.orEmpty().trim(), + baseUrl = view?.formHostValue.orEmpty().trim(), + domainSuffix = view?.formDomainSuffix.orEmpty().trim(), + defaultSymbol = view?.formSymbolValue.orEmpty().trim().getNormalizedSymbol(), + ) + when (it.data.symbols.size) { + 0 -> view?.navigateToSymbol(loginData) + else -> view?.navigateToStudentSelect( + loginData = loginData, + registerUser = it.data, + ) + } } + is Resource.Error -> { analytics.logEvent( "registration_form", @@ -173,20 +183,22 @@ class LoginAdvancedPresenter @Inject constructor( }.launch("login") } - private suspend fun getStudentsAppropriatesToLoginType(): List { + private suspend fun getStudentsAppropriatesToLoginType(): RegisterUser { val email = view?.formUsernameValue.orEmpty() val password = view?.formPassValue.orEmpty() val endpoint = view?.formHostValue.orEmpty() + val domainSuffix = view?.formDomainSuffix.orEmpty() val pin = view?.formPinValue.orEmpty() val symbol = view?.formSymbolValue.orEmpty() val token = view?.formTokenValue.orEmpty() 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.HEBE -> studentRepository.getStudentsApi(pin, symbol, token) + Sdk.Mode.SCRAPPER -> studentRepository.getUserSubjectsFromScrapper( + email, password, endpoint, domainSuffix, symbol ) + Sdk.Mode.HYBRID -> studentRepository.getStudentsHybrid( email, password, endpoint, symbol ) @@ -205,8 +217,8 @@ class LoginAdvancedPresenter @Inject constructor( var isCorrect = true - when (Sdk.Mode.valueOf(view?.formLoginType ?: "")) { - Sdk.Mode.API -> { + when (Sdk.Mode.valueOf(view?.formLoginType.orEmpty())) { + Sdk.Mode.HEBE -> { if (pin.isEmpty()) { view?.setErrorPinRequired() isCorrect = false @@ -222,17 +234,17 @@ class LoginAdvancedPresenter @Inject constructor( isCorrect = false } } + Sdk.Mode.HYBRID, Sdk.Mode.SCRAPPER -> { if (login.isEmpty()) { view?.setErrorUsernameRequired() isCorrect = false } else { - if ("@" in login && "standard" !in host) { + if ("@" in login && "login" in host) { view?.setErrorLoginRequired() isCorrect = false } - - if ("@" !in login && "standard" in host) { + if ("@" !in login && "email" in host) { view?.setErrorEmailRequired() isCorrect = false } 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 f9b84f1ab..afd33e3b6 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 @@ -1,6 +1,6 @@ package io.github.wulkanowy.ui.modules.login.advanced -import io.github.wulkanowy.data.db.entities.StudentWithSemesters +import io.github.wulkanowy.data.pojos.RegisterUser import io.github.wulkanowy.ui.base.BaseView import io.github.wulkanowy.ui.modules.login.LoginData @@ -12,6 +12,8 @@ interface LoginAdvancedView : BaseView { val formHostValue: String + val formDomainSuffix: String + val formHostSymbol: String val formLoginType: String @@ -72,7 +74,7 @@ interface LoginAdvancedView : BaseView { fun navigateToSymbol(loginData: LoginData) - fun navigateToStudentSelect(studentsWithSemesters: List) + fun navigateToStudentSelect(loginData: LoginData, registerUser: RegisterUser) 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 d31f5cf0f..1c4920696 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt @@ -7,19 +7,21 @@ import android.view.View.GONE import android.view.View.VISIBLE import androidx.core.view.isVisible import androidx.core.widget.doOnTextChanged +import androidx.fragment.app.setFragmentResultListener import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R -import io.github.wulkanowy.data.db.entities.StudentWithSemesters +import io.github.wulkanowy.data.db.entities.AdminMessage +import io.github.wulkanowy.data.pojos.RegisterUser +import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.databinding.FragmentLoginFormBinding import io.github.wulkanowy.ui.base.BaseFragment +import io.github.wulkanowy.ui.modules.captcha.CaptchaDialog +import io.github.wulkanowy.ui.modules.dashboard.viewholders.AdminMessageViewHolder import io.github.wulkanowy.ui.modules.login.LoginActivity import io.github.wulkanowy.ui.modules.login.LoginData -import io.github.wulkanowy.utils.AppInfo -import io.github.wulkanowy.utils.hideSoftInput -import io.github.wulkanowy.utils.openEmailClient -import io.github.wulkanowy.utils.openInternetBrowser -import io.github.wulkanowy.utils.setOnEditorDoneSignIn -import io.github.wulkanowy.utils.showSoftInput +import io.github.wulkanowy.ui.modules.login.support.LoginSupportDialog +import io.github.wulkanowy.ui.modules.login.support.LoginSupportInfo +import io.github.wulkanowy.utils.* import javax.inject.Inject @AndroidEntryPoint @@ -32,6 +34,9 @@ class LoginFormFragment : BaseFragment(R.layout.fragme @Inject lateinit var appInfo: AppInfo + @Inject + lateinit var preferencesRepository: PreferencesRepository + companion object { fun newInstance() = LoginFormFragment() } @@ -46,6 +51,9 @@ class LoginFormFragment : BaseFragment(R.layout.fragme get() = hostValues.getOrNull(hostKeys.indexOf(binding.loginFormHost.text.toString())) .orEmpty() + override val formDomainSuffix: String + get() = binding.loginFormDomainSuffix.text.toString() + override val formHostSymbol: String get() = hostSymbols.getOrNull(hostKeys.indexOf(binding.loginFormHost.text.toString())) .orEmpty() @@ -66,6 +74,13 @@ class LoginFormFragment : BaseFragment(R.layout.fragme super.onViewCreated(view, savedInstanceState) binding = FragmentLoginFormBinding.bind(view) presenter.onAttachView(this) + initializeCaptchaResultObserver() + } + + private fun initializeCaptchaResultObserver() { + setFragmentResultListener(CaptchaDialog.CAPTCHA_SUCCESS) { _, _ -> + presenter.onRetryAfterCaptcha() + } } override fun initView() { @@ -79,6 +94,7 @@ class LoginFormFragment : BaseFragment(R.layout.fragme loginFormUsername.doOnTextChanged { _, _, _, _ -> presenter.onUsernameTextChanged() } loginFormPass.doOnTextChanged { _, _, _, _ -> presenter.onPassTextChanged() } loginFormHost.setOnItemClickListener { _, _, _, _ -> presenter.onHostSelected() } + loginFormDomainSuffix.doOnTextChanged { _, _, _, _ -> presenter.onDomainSuffixChanged() } loginFormSignIn.setOnClickListener { presenter.onSignInClick() } loginFormAdvancedButton.setOnClickListener { presenter.onAdvancedLoginClick() } loginFormPrivacyLink.setOnClickListener { presenter.onPrivacyLinkClick() } @@ -145,12 +161,14 @@ class LoginFormFragment : BaseFragment(R.layout.fragme override fun setErrorPassRequired(focus: Boolean) { with(binding.loginFormPassLayout) { error = getString(R.string.error_field_required) + setEndIconTintList(requireContext().getAttrColorStateList(R.attr.colorError)) } } override fun setErrorPassInvalid(focus: Boolean) { with(binding.loginFormPassLayout) { error = getString(R.string.login_invalid_password) + setEndIconTintList(requireContext().getAttrColorStateList(R.attr.colorError)) } } @@ -158,6 +176,7 @@ class LoginFormFragment : BaseFragment(R.layout.fragme with(binding) { loginFormUsernameLayout.error = " " loginFormPassLayout.error = " " + loginFormPassLayout.setEndIconTintList(requireContext().getAttrColorStateList(R.attr.colorError)) loginFormHostLayout.error = " " loginFormErrorBox.text = message ?: getString(R.string.login_incorrect_password_default) loginFormErrorBox.isVisible = true @@ -170,6 +189,12 @@ class LoginFormFragment : BaseFragment(R.layout.fragme } } + override fun setDomainSuffixInvalid() { + with(binding.loginFormDomainSuffixLayout) { + error = getString(R.string.login_invalid_domain_suffix) + } + } + override fun clearUsernameError() { binding.loginFormUsernameLayout.error = null binding.loginFormErrorBox.isVisible = false @@ -177,6 +202,9 @@ class LoginFormFragment : BaseFragment(R.layout.fragme override fun clearPassError() { binding.loginFormPassLayout.error = null + binding.loginFormPassLayout.setEndIconTintList( + requireContext().getAttrColorStateList(R.attr.colorOnSurface) + ) binding.loginFormErrorBox.isVisible = false } @@ -185,6 +213,10 @@ class LoginFormFragment : BaseFragment(R.layout.fragme binding.loginFormErrorBox.isVisible = false } + override fun clearDomainSuffixError() { + binding.loginFormDomainSuffixLayout.error = null + } + override fun showSoftKeyboard() { activity?.showSoftInput() } @@ -201,14 +233,34 @@ class LoginFormFragment : BaseFragment(R.layout.fragme binding.loginFormContainer.visibility = if (show) VISIBLE else GONE } + override fun showAdminMessage(message: AdminMessage?) { + AdminMessageViewHolder( + binding = binding.loginFormMessage, + onAdminMessageDismissClickListener = presenter::onAdminMessageDismissed, + onAdminMessageClickListener = presenter::onAdminMessageSelected, + ).bind(message) + binding.loginFormMessage.root.isVisible = message != null + } + + override fun openInternetBrowser(url: String) { + requireContext().openInternetBrowser(url) + } + + override fun showDomainSuffixInput(show: Boolean) { + binding.loginFormDomainSuffixLayout.isVisible = show + } + + override fun showOtherOptionsButton(show: Boolean) { + binding.loginFormAdvancedButton.isVisible = show + } + @SuppressLint("SetTextI18n") override fun showVersion() { binding.loginFormVersion.text = "v${appInfo.versionName}" } override fun showContact(show: Boolean) { - binding.loginFormContact.visibility = if (show) VISIBLE else GONE - binding.loginFormRecoverLink.visibility = if (show) GONE else VISIBLE + binding.loginFormContact.isVisible = show } override fun openPrivacyPolicyPage() { @@ -222,8 +274,8 @@ class LoginFormFragment : BaseFragment(R.layout.fragme (activity as? LoginActivity)?.navigateToSymbolFragment(loginData) } - override fun navigateToStudentSelect(studentsWithSemesters: List) { - (activity as? LoginActivity)?.navigateToStudentSelect(studentsWithSemesters) + override fun navigateToStudentSelect(loginData: LoginData, registerUser: RegisterUser) { + (activity as? LoginActivity)?.navigateToStudentSelect(loginData, registerUser) } override fun openAdvancedLogin() { @@ -249,21 +301,10 @@ class LoginFormFragment : BaseFragment(R.layout.fragme override fun onResume() { super.onResume() presenter.updateUsernameLabel() + presenter.updateCustomDomainSuffixVisibility() } - override fun openEmail(lastError: String) { - context?.openEmailClient( - chooserTitle = requireContext().getString(R.string.login_email_intent_title), - email = "wulkanowyinc@gmail.com", - subject = requireContext().getString(R.string.login_email_subject), - body = requireContext().getString( - R.string.login_email_text, - "${appInfo.systemManufacturer} ${appInfo.systemModel}", - appInfo.systemVersion.toString(), - appInfo.versionName, - "$formHostValue/$formHostSymbol", - lastError - ) - ) + override fun openEmail(supportInfo: LoginSupportInfo) { + LoginSupportDialog.newInstance(supportInfo).show(childFragmentManager, "support_dialog") } } 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 0acb0ea6d..69e1d027d 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,12 +1,25 @@ package io.github.wulkanowy.ui.modules.login.form import androidx.core.net.toUri -import io.github.wulkanowy.data.* +import io.github.wulkanowy.data.db.entities.AdminMessage +import io.github.wulkanowy.data.enums.MessageType +import io.github.wulkanowy.data.flatResourceFlow +import io.github.wulkanowy.data.logResourceStatus +import io.github.wulkanowy.data.onResourceData +import io.github.wulkanowy.data.onResourceError +import io.github.wulkanowy.data.onResourceLoading +import io.github.wulkanowy.data.onResourceNotLoading +import io.github.wulkanowy.data.onResourceSuccess +import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.StudentRepository +import io.github.wulkanowy.data.resourceFlow +import io.github.wulkanowy.domain.adminmessage.GetAppropriateAdminMessageUseCase 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.ui.modules.login.support.LoginSupportInfo import io.github.wulkanowy.utils.AnalyticsHelper +import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.ifNullOrBlank import timber.log.Timber import java.net.URL @@ -15,7 +28,10 @@ import javax.inject.Inject class LoginFormPresenter @Inject constructor( studentRepository: StudentRepository, private val loginErrorHandler: LoginErrorHandler, - private val analytics: AnalyticsHelper + private val appInfo: AppInfo, + private val analytics: AnalyticsHelper, + private val getAppropriateAdminMessageUseCase: GetAppropriateAdminMessageUseCase, + private val preferencesRepository: PreferencesRepository, ) : BasePresenter(loginErrorHandler, studentRepository) { private var lastError: Throwable? = null @@ -25,6 +41,7 @@ class LoginFormPresenter @Inject constructor( view.run { initView() showContact(false) + showOtherOptionsButton(appInfo.isDebug) showVersion() loginErrorHandler.onBadCredentials = { @@ -33,6 +50,31 @@ class LoginFormPresenter @Inject constructor( Timber.i("Entered wrong username or password") } } + + reloadAdminMessage() + } + + private fun reloadAdminMessage() { + flatResourceFlow { + getAppropriateAdminMessageUseCase( + scrapperBaseUrl = view?.formHostValue.orEmpty(), + type = MessageType.LOGIN_MESSAGE, + ) + } + .logResourceStatus("load login admin message") + .onResourceData { view?.showAdminMessage(it) } + .onResourceError { view?.showAdminMessage(null) } + .launch() + } + + fun onAdminMessageSelected(url: String?) { + url?.let { view?.openInternetBrowser(it) } + } + + fun onAdminMessageDismissed(adminMessage: AdminMessage) { + preferencesRepository.dismissedAdminMessageIds += adminMessage.id + + view?.showAdminMessage(null) } fun onPrivacyLinkClick() { @@ -48,12 +90,26 @@ class LoginFormPresenter @Inject constructor( clearPassError() clearUsernameError() clearHostError() - if (formHostValue.contains("fakelog")) { + if (formHostValue.contains("wulkanowy")) { setCredentials("jan@fakelog.cf", "jan123") } else if (formUsernameValue == "jan@fakelog.cf" && formPassValue == "jan123") { setCredentials("", "") } + updateCustomDomainSuffixVisibility() updateUsernameLabel() + reloadAdminMessage() + } + } + + fun onDomainSuffixChanged() { + view?.apply { + clearDomainSuffixError() + } + } + + fun updateCustomDomainSuffixVisibility() { + view?.run { + showDomainSuffixInput("customSuffix" in formHostValue) } } @@ -84,20 +140,40 @@ class LoginFormPresenter @Inject constructor( } } - fun onSignInClick() { + private fun getLoginData(): LoginData { val email = view?.formUsernameValue.orEmpty().trim() val password = view?.formPassValue.orEmpty().trim() val host = view?.formHostValue.orEmpty().trim() + val domainSuffix = view?.formDomainSuffix.orEmpty().trim().takeIf { + "customSuffix" in host + }.orEmpty() val symbol = view?.formHostSymbol.orEmpty().trim() - if (!validateCredentials(email, password, host)) return + return LoginData( + login = email, + password = password, + baseUrl = host, + domainSuffix = domainSuffix, + defaultSymbol = symbol + ) + } + + fun onRetryAfterCaptcha() { + onSignInClick() + } + + fun onSignInClick() { + val loginData = getLoginData() + + if (!validateCredentials(loginData)) return resourceFlow { - studentRepository.getStudentsScrapper( - email = email, - password = password, - scrapperBaseUrl = host, - symbol = symbol + studentRepository.getUserSubjectsFromScrapper( + email = loginData.login, + password = loginData.password, + scrapperBaseUrl = loginData.baseUrl, + domainSuffix = loginData.domainSuffix, + symbol = loginData.defaultSymbol, ) } .logResourceStatus("login") @@ -109,15 +185,14 @@ class LoginFormPresenter @Inject constructor( } } .onResourceSuccess { - when (it.size) { - 0 -> view?.navigateToSymbol(LoginData(email, password, host)) - else -> view?.navigateToStudentSelect(it) + when (it.symbols.size) { + 0 -> view?.navigateToSymbol(loginData) + else -> view?.navigateToStudentSelect(loginData, it) } analytics.logEvent( "registration_form", "success" to true, - "students" to it.size, - "scrapperBaseUrl" to host, + "scrapperBaseUrl" to loginData.baseUrl, "error" to "No error" ) } @@ -134,8 +209,7 @@ class LoginFormPresenter @Inject constructor( analytics.logEvent( "registration_form", "success" to false, - "students" to -1, - "scrapperBaseUrl" to host, + "scrapperBaseUrl" to loginData.baseUrl, "error" to it.message.ifNullOrBlank { "No message" } ) } @@ -147,31 +221,43 @@ class LoginFormPresenter @Inject constructor( } fun onEmailClick() { - view?.openEmail(lastError?.message.ifNullOrBlank { "none" }) + view?.openEmail( + LoginSupportInfo( + loginData = getLoginData(), + lastErrorMessage = lastError?.message, + registerUser = null, + enteredSymbol = null, + ) + ) } fun onRecoverClick() { view?.onRecoverClick() } - private fun validateCredentials(login: String, password: String, host: String): Boolean { + private fun validateCredentials(loginData: LoginData): Boolean { var isCorrect = true - if (login.isEmpty()) { + if (loginData.login.isEmpty()) { view?.setErrorUsernameRequired() isCorrect = false } else { - if ("@" in login && "login" in host) { + if ("@" in loginData.login && "login" in loginData.baseUrl) { view?.setErrorLoginRequired() isCorrect = false } - if ("@" !in login && "email" in host) { + if ("@" !in loginData.login && "email" in loginData.baseUrl) { view?.setErrorEmailRequired() isCorrect = false } - if ("@" in login && "||" !in login && "login" !in host && "email" !in host) { - val emailHost = login.substringAfter("@") - val emailDomain = URL(host).host + + val isEmailLogin = "@" in loginData.login + val isEmailWithLogin = "||" !in loginData.login + val isLoginNotRequired = "login" !in loginData.baseUrl + val isEmailNotRequired = "email" !in loginData.baseUrl + if (isEmailLogin && isEmailWithLogin && isLoginNotRequired && isEmailNotRequired) { + val emailHost = loginData.login.substringAfter("@") + val emailDomain = URL(loginData.baseUrl).host if (!emailHost.equals(emailDomain, true)) { view?.setErrorEmailInvalid(domain = emailDomain) isCorrect = false @@ -179,16 +265,21 @@ class LoginFormPresenter @Inject constructor( } } - if (password.isEmpty()) { + if (loginData.password.isEmpty()) { view?.setErrorPassRequired(focus = isCorrect) isCorrect = false } - if (password.length < 6 && password.isNotEmpty()) { + if (loginData.password.length < 6 && loginData.password.isNotEmpty()) { view?.setErrorPassInvalid(focus = isCorrect) isCorrect = false } + if (loginData.domainSuffix !in listOf("", "rc", "kurs")) { + view?.setDomainSuffixInvalid() + isCorrect = false + } + return isCorrect } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormView.kt index 8003975db..6ea22d180 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 @@ -1,8 +1,10 @@ package io.github.wulkanowy.ui.modules.login.form -import io.github.wulkanowy.data.db.entities.StudentWithSemesters +import io.github.wulkanowy.data.db.entities.AdminMessage +import io.github.wulkanowy.data.pojos.RegisterUser import io.github.wulkanowy.ui.base.BaseView import io.github.wulkanowy.ui.modules.login.LoginData +import io.github.wulkanowy.ui.modules.login.support.LoginSupportInfo interface LoginFormView : BaseView { @@ -14,6 +16,8 @@ interface LoginFormView : BaseView { val formHostValue: String + val formDomainSuffix: String + val formHostSymbol: String val nicknameLabel: String @@ -42,12 +46,16 @@ interface LoginFormView : BaseView { fun setErrorEmailInvalid(domain: String) + fun setDomainSuffixInvalid() + fun clearUsernameError() fun clearPassError() fun clearHostError() + fun clearDomainSuffixError() + fun showSoftKeyboard() fun hideSoftKeyboard() @@ -56,11 +64,19 @@ interface LoginFormView : BaseView { fun showContent(show: Boolean) + fun showAdminMessage(message: AdminMessage?) + + fun openInternetBrowser(url: String) + + fun showDomainSuffixInput(show: Boolean) + + fun showOtherOptionsButton(show: Boolean) + fun showVersion() fun navigateToSymbol(loginData: LoginData) - fun navigateToStudentSelect(studentsWithSemesters: List) + fun navigateToStudentSelect(loginData: LoginData, registerUser: RegisterUser) fun openPrivacyPolicyPage() @@ -68,7 +84,7 @@ interface LoginFormView : BaseView { fun openFaqPage() - fun openEmail(lastError: String) + fun openEmail(supportInfo: LoginSupportInfo) fun openAdvancedLogin() 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 786bbfce8..b9afba986 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 @@ -98,7 +98,7 @@ class LoginRecoverFragment : loginRecoverButton.setOnClickListener { presenter.onRecoverClick() } loginRecoverErrorRetry.setOnClickListener { presenter.onRecoverClick() } loginRecoverErrorDetails.setOnClickListener { presenter.onDetailsClick() } - loginRecoverLogin.setOnClickListener { (activity as LoginActivity).onBackPressed() } + loginRecoverLogin.setOnClickListener { (activity as LoginActivity).onBackPressedDispatcher.onBackPressed() } } with(bindingLocal.loginRecoverHost) { 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 3d0493012..18902e014 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/recover/LoginRecoverPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/recover/LoginRecoverPresenter.kt @@ -38,7 +38,7 @@ class LoginRecoverPresenter @Inject constructor( fun onHostSelected() { view?.run { - if ("fakelog" in recoverHostValue) setDefaultCredentials("jan@fakelog.cf") + if ("wulkanowy" in recoverHostValue) setDefaultCredentials("jan@fakelog.cf") clearUsernameError() updateFields() } @@ -46,7 +46,7 @@ class LoginRecoverPresenter @Inject constructor( fun updateFields() { view?.run { - setUsernameHint(if ("standard" in recoverHostValue) emailHintString else loginPeselEmailHintString) + setUsernameHint(if ("email" in recoverHostValue) emailHintString else loginPeselEmailHintString) } } @@ -60,7 +60,7 @@ class LoginRecoverPresenter @Inject constructor( resourceFlow { recoverRepository.getReCaptchaSiteKey( host, - symbol.ifBlank { "Default" }) + symbol.ifBlank { "default" }) }.onEach { when (it) { is Resource.Loading -> view?.run { @@ -92,7 +92,7 @@ class LoginRecoverPresenter @Inject constructor( isCorrect = false } - if ("standard" in host && "@" !in username) { + if ("email" in host && "@" !in username) { view?.setUsernameError(view?.invalidEmailString.orEmpty()) isCorrect = false } @@ -103,7 +103,7 @@ class LoginRecoverPresenter @Inject constructor( fun onReCaptchaVerified(reCaptchaResponse: String) { val username = view?.recoverNameValue.orEmpty() val host = view?.recoverHostValue.orEmpty() - val symbol = view?.formHostSymbol.ifNullOrBlank { "Default" } + val symbol = view?.formHostSymbol.ifNullOrBlank { "default" } resourceFlow { recoverRepository.sendRecoverRequest( diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectAdapter.kt index c046c2ff5..e6d131829 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectAdapter.kt @@ -2,65 +2,182 @@ package io.github.wulkanowy.ui.modules.login.studentselect import android.annotation.SuppressLint import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup +import androidx.core.view.isVisible +import androidx.recyclerview.widget.DiffUtil.ItemCallback +import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView -import io.github.wulkanowy.data.db.entities.StudentWithSemesters -import io.github.wulkanowy.databinding.ItemLoginStudentSelectBinding +import io.github.wulkanowy.R +import io.github.wulkanowy.databinding.* import javax.inject.Inject +@SuppressLint("SetTextI18n") class LoginStudentSelectAdapter @Inject constructor() : - RecyclerView.Adapter() { + ListAdapter(Differ) { - private val checkedList = mutableMapOf() + override fun getItemViewType(position: Int): Int = getItem(position).type.ordinal - var items = emptyList>() - set(value) { - field = value - checkedList.clear() + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + val inflater = LayoutInflater.from(parent.context) + return when (LoginStudentSelectItemType.values()[viewType]) { + LoginStudentSelectItemType.EMPTY_SYMBOLS_HEADER -> EmptySymbolsHeaderViewHolder( + ItemLoginStudentSelectEmptySymbolHeaderBinding.inflate(inflater, parent, false), + ) + LoginStudentSelectItemType.SYMBOL_HEADER -> SymbolsHeaderViewHolder( + ItemLoginStudentSelectHeaderSymbolBinding.inflate(inflater, parent, false) + ) + LoginStudentSelectItemType.SCHOOL_HEADER -> SchoolHeaderViewHolder( + ItemLoginStudentSelectHeaderSchoolBinding.inflate(inflater, parent, false) + ) + LoginStudentSelectItemType.STUDENT -> StudentViewHolder( + ItemLoginStudentSelectStudentBinding.inflate(inflater, parent, false) + ) + LoginStudentSelectItemType.HELP -> HelpViewHolder( + ItemLoginStudentSelectHelpBinding.inflate(inflater, parent, false) + ) } + } - var onClickListener: (StudentWithSemesters, alreadySaved: Boolean) -> Unit = { _, _ -> } + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + when (holder) { + is EmptySymbolsHeaderViewHolder -> holder.bind(getItem(position) as LoginStudentSelectItem.EmptySymbolsHeader) + is SymbolsHeaderViewHolder -> holder.bind(getItem(position) as LoginStudentSelectItem.SymbolHeader) + is SchoolHeaderViewHolder -> holder.bind(getItem(position) as LoginStudentSelectItem.SchoolHeader) + is StudentViewHolder -> holder.bind(getItem(position) as LoginStudentSelectItem.Student) + is HelpViewHolder -> holder.bind(getItem(position) as LoginStudentSelectItem.Help) + } + } - override fun getItemCount() = items.size + private class EmptySymbolsHeaderViewHolder( + private val binding: ItemLoginStudentSelectEmptySymbolHeaderBinding, + ) : RecyclerView.ViewHolder(binding.root) { - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ItemViewHolder( - ItemLoginStudentSelectBinding.inflate(LayoutInflater.from(parent.context), parent, false) - ) - - @SuppressLint("SetTextI18n") - override fun onBindViewHolder(holder: ItemViewHolder, position: Int) { - val (studentAndSemesters, alreadySaved) = items[position] - val student = studentAndSemesters.student - val semesters = studentAndSemesters.semesters - val diary = semesters.maxByOrNull { it.semesterId } - - with(holder.binding) { - loginItemName.text = "${student.studentName} ${diary?.diaryName.orEmpty()}" - loginItemSchool.text = student.schoolName - loginItemName.isEnabled = !alreadySaved - loginItemSchool.isEnabled = !alreadySaved - loginItemSignedIn.visibility = if (alreadySaved) View.VISIBLE else View.GONE - - with(loginItemCheck) { - isEnabled = !alreadySaved - keyListener = null - isChecked = checkedList[position] ?: false + fun bind(item: LoginStudentSelectItem.EmptySymbolsHeader) { + with(binding) { + loginStudentSelectEmptySymbolChevron.rotation = if (item.isExpanded) 270f else 90f + root.setOnClickListener { item.onClick() } } + } + } - root.setOnClickListener { - onClickListener(studentAndSemesters, alreadySaved) + private class SymbolsHeaderViewHolder( + private val binding: ItemLoginStudentSelectHeaderSymbolBinding, + ) : RecyclerView.ViewHolder(binding.root) { + + fun bind(item: LoginStudentSelectItem.SymbolHeader) { + with(binding) { + loginStudentSelectHeaderSymbolValue.text = buildString { + append(root.context.getString(R.string.mobile_device_symbol)) + append(": ") + append(item.humanReadableName ?: item.symbol.symbol) + if (!item.humanReadableName.isNullOrBlank()) { + append(" (${item.symbol.symbol})") + } + } + loginStudentSelectHeaderSymbolUsername.text = item.symbol.userName + loginStudentSelectHeaderSymbolUsername.isVisible = item.symbol.userName.isNotBlank() + loginStudentSelectHeaderSymbolError.text = item.symbol.error?.message + loginStudentSelectHeaderSymbolError.isVisible = item.symbol.error != null + loginStudentSelectHeaderSymbolError.maxLines = when { + item.isErrorExpanded -> Int.MAX_VALUE + else -> 2 + } + + if (item.symbol.error != null) { + root.setOnClickListener { item.onClick(item.symbol) } + } else root.setOnClickListener(null) + } + } + } + + private class SchoolHeaderViewHolder( + private val binding: ItemLoginStudentSelectHeaderSchoolBinding, + ) : RecyclerView.ViewHolder(binding.root) { + + fun bind(item: LoginStudentSelectItem.SchoolHeader) { + with(binding) { + loginStudentSelectHeaderSchoolName.text = buildString { + append(item.unit.schoolName.trim()) + append(" (") + append(item.unit.schoolShortName) + append(")") + } + loginStudentSelectHeaderSchoolDetails.isVisible = item.unit.students.isEmpty() + loginStudentSelectHeaderSchoolError.text = item.unit.error?.message + loginStudentSelectHeaderSchoolError.isVisible = item.unit.error != null + loginStudentSelectHeaderSchoolError.maxLines = when { + item.isErrorExpanded -> Int.MAX_VALUE + else -> 2 + } + + if (item.unit.error != null) { + root.setOnClickListener { item.onClick(item.unit) } + } else root.setOnClickListener(null) + } + } + } + + private class StudentViewHolder( + private val binding: ItemLoginStudentSelectStudentBinding, + ) : RecyclerView.ViewHolder(binding.root) { + + fun bind(item: LoginStudentSelectItem.Student) { + val student = item.student + val semesters = student.semesters + val diary = semesters.maxByOrNull { it.semesterId } + + with(binding) { + loginItemName.text = "${student.studentName} ${student.studentSurname}" + loginItemName.isEnabled = item.isEnabled + loginItemSignedIn.text = if (!item.isEnabled) { + root.context.getString(R.string.login_signed_in) + } else diary?.diaryName with(loginItemCheck) { - if (isEnabled) { - isChecked = !isChecked - checkedList[position] = isChecked - } + keyListener = null + isEnabled = item.isEnabled + isChecked = item.isSelected || !item.isEnabled + } + + root.isEnabled = item.isEnabled + root.setOnClickListener { + item.onClick(item) } } } } - class ItemViewHolder(val binding: ItemLoginStudentSelectBinding) : - RecyclerView.ViewHolder(binding.root) + private class HelpViewHolder( + private val binding: ItemLoginStudentSelectHelpBinding, + ) : RecyclerView.ViewHolder(binding.root) { + + fun bind(item: LoginStudentSelectItem.Help) { + with(binding) { + loginStudentSelectHelpSymbol.isVisible = item.isSymbolButtonVisible + loginStudentSelectHelpSymbol.setOnClickListener { item.onEnterSymbolClick() } + loginStudentSelectHelpMail.setOnClickListener { item.onContactUsClick() } + loginStudentSelectHelpDiscord.setOnClickListener { item.onDiscordClick() } + } + } + } + + private object Differ : ItemCallback() { + + override fun areItemsTheSame( + oldItem: LoginStudentSelectItem, newItem: LoginStudentSelectItem + ): Boolean = when { + oldItem is LoginStudentSelectItem.EmptySymbolsHeader && newItem is LoginStudentSelectItem.EmptySymbolsHeader -> true + oldItem is LoginStudentSelectItem.SymbolHeader && newItem is LoginStudentSelectItem.SymbolHeader -> { + oldItem.symbol == newItem.symbol + } + oldItem is LoginStudentSelectItem.Student && newItem is LoginStudentSelectItem.Student -> { + oldItem.student == newItem.student + } + else -> oldItem == newItem + } + + override fun areContentsTheSame( + oldItem: LoginStudentSelectItem, newItem: LoginStudentSelectItem + ): Boolean = oldItem == newItem + } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectFragment.kt index 6c910fe03..0fe36aa99 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 @@ -2,20 +2,21 @@ package io.github.wulkanowy.ui.modules.login.studentselect 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 androidx.core.view.isVisible import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R -import io.github.wulkanowy.data.db.entities.StudentWithSemesters +import io.github.wulkanowy.data.pojos.RegisterUser +import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.databinding.FragmentLoginStudentSelectBinding import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.modules.login.LoginActivity -import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.ui.modules.login.LoginData +import io.github.wulkanowy.ui.modules.login.support.LoginSupportDialog +import io.github.wulkanowy.ui.modules.login.support.LoginSupportInfo import io.github.wulkanowy.utils.AppInfo -import io.github.wulkanowy.utils.openEmailClient import io.github.wulkanowy.utils.openInternetBrowser +import io.github.wulkanowy.utils.serializable import javax.inject.Inject @AndroidEntryPoint @@ -32,91 +33,86 @@ class LoginStudentSelectFragment : @Inject lateinit var appInfo: AppInfo - companion object { - const val ARG_STUDENTS = "STUDENTS" + @Inject + lateinit var preferencesRepository: PreferencesRepository - fun newInstance(studentsWithSemesters: List) = + private lateinit var symbolsNames: Array + private lateinit var symbolsValues: Array + + override val symbols: Map by lazy { + symbolsValues.zip(symbolsNames).toMap() + } + + companion object { + private const val ARG_LOGIN = "LOGIN" + private const val ARG_STUDENTS = "STUDENTS" + + fun newInstance(loginData: LoginData, registerUser: RegisterUser) = LoginStudentSelectFragment().apply { - arguments = bundleOf(ARG_STUDENTS to studentsWithSemesters) + arguments = bundleOf( + ARG_LOGIN to loginData, + ARG_STUDENTS to registerUser, + ) } } - @Suppress("UNCHECKED_CAST") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding = FragmentLoginStudentSelectBinding.bind(view) + + symbolsNames = resources.getStringArray(R.array.symbols) + symbolsValues = resources.getStringArray(R.array.symbols_values) + presenter.onAttachView( view = this, - students = requireArguments().getSerializable(ARG_STUDENTS) as List, + loginData = requireArguments().serializable(ARG_LOGIN), + registerUser = requireArguments().serializable(ARG_STUDENTS), ) } override fun initView() { (requireActivity() as LoginActivity).showActionBar(true) - loginAdapter.onClickListener = presenter::onItemSelected - with(binding) { loginStudentSelectSignIn.setOnClickListener { presenter.onSignIn() } - loginStudentSelectContactDiscord.setOnClickListener { presenter.onDiscordClick() } - loginStudentSelectContactEmail.setOnClickListener { presenter.onEmailClick() } - - with(loginStudentSelectRecycler) { - layoutManager = LinearLayoutManager(context) - adapter = loginAdapter - } + loginStudentSelectRecycler.adapter = loginAdapter } } - override fun updateData(data: List>) { - with(loginAdapter) { - items = data - notifyDataSetChanged() - } + override fun updateData(data: List) { + loginAdapter.submitList(data) } - override fun openMainView() { - startActivity(MainActivity.getStartIntent(requireContext())) - requireActivity().finish() + override fun navigateToSymbol(loginData: LoginData) { + (requireActivity() as LoginActivity).navigateToSymbolFragment(loginData) + } + + override fun navigateToNext() { + (requireActivity() as LoginActivity).navigateToNotifications() } override fun showProgress(show: Boolean) { - binding.loginStudentSelectProgress.visibility = if (show) VISIBLE else GONE + binding.loginStudentSelectProgress.isVisible = show } override fun showContent(show: Boolean) { - binding.loginStudentSelectContent.visibility = if (show) VISIBLE else GONE + binding.loginStudentSelectContent.isVisible = show } override fun enableSignIn(enable: Boolean) { binding.loginStudentSelectSignIn.isEnabled = enable } - override fun showContact(show: Boolean) { - binding.loginStudentSelectContact.visibility = if (show) VISIBLE else GONE + override fun openDiscordInvite() { + context?.openInternetBrowser("https://discord.gg/vccAQBr", ::showMessage) + } + + override fun openEmail(supportInfo: LoginSupportInfo) { + LoginSupportDialog.newInstance(supportInfo).show(childFragmentManager, "support_dialog") } override fun onDestroyView() { presenter.onDetachView() super.onDestroyView() } - - override fun openDiscordInvite() { - context?.openInternetBrowser("https://discord.gg/vccAQBr", ::showMessage) - } - - override fun openEmail(lastError: String) { - context?.openEmailClient( - chooserTitle = requireContext().getString(R.string.login_email_intent_title), - email = "wulkanowyinc@gmail.com", - subject = requireContext().getString(R.string.login_email_subject), - body = requireContext().getString( - R.string.login_email_text, appInfo.systemModel, - appInfo.systemVersion.toString(), - appInfo.versionName, - "Select users to log in", - lastError - ) - ) - } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectItem.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectItem.kt new file mode 100644 index 000000000..1edc8e7b4 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectItem.kt @@ -0,0 +1,50 @@ +package io.github.wulkanowy.ui.modules.login.studentselect + +import io.github.wulkanowy.data.pojos.RegisterStudent +import io.github.wulkanowy.data.pojos.RegisterSymbol +import io.github.wulkanowy.data.pojos.RegisterUnit + +sealed class LoginStudentSelectItem(val type: LoginStudentSelectItemType) { + + data class EmptySymbolsHeader( + val isExpanded: Boolean, + val onClick: () -> Unit, + ) : LoginStudentSelectItem(LoginStudentSelectItemType.EMPTY_SYMBOLS_HEADER) + + data class SymbolHeader( + val symbol: RegisterSymbol, + val humanReadableName: String?, + val isErrorExpanded: Boolean, + val onClick: (RegisterSymbol) -> Unit, + ) : LoginStudentSelectItem(LoginStudentSelectItemType.SYMBOL_HEADER) + + data class SchoolHeader( + val unit: RegisterUnit, + val isErrorExpanded: Boolean, + val onClick: (RegisterUnit) -> Unit, + ) : LoginStudentSelectItem(LoginStudentSelectItemType.SCHOOL_HEADER) + + data class Student( + val symbol: RegisterSymbol, + val unit: RegisterUnit, + val student: RegisterStudent, + val isEnabled: Boolean, + val isSelected: Boolean, + val onClick: (Student) -> Unit, + ) : LoginStudentSelectItem(LoginStudentSelectItemType.STUDENT) + + data class Help( + val onEnterSymbolClick: () -> Unit, + val onContactUsClick: () -> Unit, + val onDiscordClick: () -> Unit, + val isSymbolButtonVisible: Boolean, + ) : LoginStudentSelectItem(LoginStudentSelectItemType.HELP) +} + +enum class LoginStudentSelectItemType { + EMPTY_SYMBOLS_HEADER, + SYMBOL_HEADER, + SCHOOL_HEADER, + STUDENT, + HELP, +} 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 3455b3cf1..344414180 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,36 +1,59 @@ package io.github.wulkanowy.ui.modules.login.studentselect import io.github.wulkanowy.data.Resource -import io.github.wulkanowy.data.db.entities.Student +import io.github.wulkanowy.data.dataOrNull import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.data.logResourceStatus +import io.github.wulkanowy.data.mappers.mapToStudentWithSemesters +import io.github.wulkanowy.data.pojos.RegisterStudent +import io.github.wulkanowy.data.pojos.RegisterSymbol +import io.github.wulkanowy.data.pojos.RegisterUnit +import io.github.wulkanowy.data.pojos.RegisterUser +import io.github.wulkanowy.data.repositories.SchoolsRepository import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.resourceFlow +import io.github.wulkanowy.sdk.scrapper.login.InvalidSymbolException import io.github.wulkanowy.services.sync.SyncManager 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.ui.modules.login.support.LoginSupportInfo import io.github.wulkanowy.utils.AnalyticsHelper -import io.github.wulkanowy.utils.ifNullOrBlank +import io.github.wulkanowy.utils.AppInfo +import io.github.wulkanowy.utils.isCurrent import kotlinx.coroutines.flow.onEach import timber.log.Timber import javax.inject.Inject class LoginStudentSelectPresenter @Inject constructor( studentRepository: StudentRepository, + private val schoolsRepository: SchoolsRepository, private val loginErrorHandler: LoginErrorHandler, private val syncManager: SyncManager, - private val analytics: AnalyticsHelper + private val analytics: AnalyticsHelper, + private val appInfo: AppInfo, ) : BasePresenter(loginErrorHandler, studentRepository) { private var lastError: Throwable? = null - private val selectedStudents = mutableListOf() + private lateinit var registerUser: RegisterUser + private lateinit var loginData: LoginData - fun onAttachView(view: LoginStudentSelectView, students: List) { + private lateinit var students: List + private var isEmptySymbolsExpanded = false + private var expandedSymbolError: RegisterSymbol? = null + private var expandedSchoolError: RegisterUnit? = null + + private val selectedStudents = mutableListOf() + + fun onAttachView( + view: LoginStudentSelectView, + loginData: LoginData, + registerUser: RegisterUser, + ) { super.onAttachView(view) with(view) { initView() - showContact(false) enableSignIn(false) loginErrorHandler.onStudentDuplicate = { showMessage(it) @@ -38,50 +61,195 @@ class LoginStudentSelectPresenter @Inject constructor( } } - if (students.size == 1) registerStudents(students) - loadData(students) + this.loginData = loginData + this.registerUser = registerUser + loadData() } + private fun loadData() { + resetSelectedState() + + resourceFlow { studentRepository.getSavedStudents(false) }.onEach { + students = it.dataOrNull.orEmpty() + when (it) { + is Resource.Loading -> Timber.d("Login student select students load started") + is Resource.Success -> { + getStudentsWithCurrentlyActiveSemesters() + selectedStudents.clear() + selectedStudents.addAll(getStudentsWithCurrentlyActiveSemesters()) + view?.enableSignIn(selectedStudents.isNotEmpty()) + refreshItems() + } + + is Resource.Error -> { + errorHandler.dispatch(it.error) + lastError = it.error + refreshItems() + } + } + }.launch() + } + + private fun getStudentsWithCurrentlyActiveSemesters(): List { + val students = registerUser.symbols.flatMap { symbol -> + symbol.schools.flatMap { unit -> + unit.students.map { + createStudentItem(it, symbol, unit, students) + } + } + } + return students + .filter { it.isEnabled } + .filter { student -> + student.student.semesters.any { semester -> + semester.isCurrent() + } + } + } + + private fun createItems(): List = buildList { + val notEmptySymbols = registerUser.symbols.filter { it.schools.isNotEmpty() } + val emptySymbols = registerUser.symbols.filter { it.schools.isEmpty() } + + if (emptySymbols.isNotEmpty() && notEmptySymbols.isNotEmpty() && emptySymbols.any { it.symbol == loginData.userEnteredSymbol }) { + add(createEmptySymbolItem(emptySymbols.first { it.symbol == loginData.userEnteredSymbol })) + } + + addAll(createNotEmptySymbolItems(notEmptySymbols, students)) + addAll(createEmptySymbolItems(emptySymbols, notEmptySymbols.isNotEmpty())) + + val helpItem = LoginStudentSelectItem.Help( + onEnterSymbolClick = ::onEnterSymbol, + onContactUsClick = ::onEmailClick, + onDiscordClick = ::onDiscordClick, + isSymbolButtonVisible = "login" !in loginData.baseUrl, + ) + add(helpItem) + } + + private fun createNotEmptySymbolItems( + notEmptySymbols: List, + students: List, + ) = buildList { + notEmptySymbols.forEach { registerSymbol -> + val symbolHeader = LoginStudentSelectItem.SymbolHeader( + symbol = registerSymbol, + humanReadableName = view?.symbols?.get(registerSymbol.symbol), + isErrorExpanded = expandedSymbolError == registerSymbol, + onClick = ::onSymbolItemClick, + ) + add(symbolHeader) + + registerSymbol.schools.forEach { registerUnit -> + val schoolHeader = LoginStudentSelectItem.SchoolHeader( + unit = registerUnit, + isErrorExpanded = expandedSchoolError == registerUnit, + onClick = ::onUnitItemClick, + ) + add(schoolHeader) + + registerUnit.students.forEach { + add(createStudentItem(it, registerSymbol, registerUnit, students)) + } + } + } + } + + private fun createStudentItem( + student: RegisterStudent, + symbol: RegisterSymbol, + school: RegisterUnit, + students: List, + ) = LoginStudentSelectItem.Student( + symbol = symbol, + unit = school, + student = student, + onClick = ::onItemSelected, + isEnabled = students.none { + it.student.email == registerUser.login + && it.student.symbol == symbol.symbol + && it.student.studentId == student.studentId + && it.student.schoolSymbol == school.schoolId + && it.student.classId == student.classId + }, + isSelected = selectedStudents + .filter { it.symbol.symbol == symbol.symbol } + .filter { it.unit.schoolId == school.schoolId } + .filter { it.student.studentId == student.studentId } + .filter { it.student.classId == student.classId } + .size == 1, + ) + + private fun createEmptySymbolItems( + emptySymbols: List, + isNotEmptySymbolsExist: Boolean, + ) = buildList { + val filteredEmptySymbols = emptySymbols.filter { + it.error !is InvalidSymbolException + }.ifEmpty { emptySymbols.takeIf { !isNotEmptySymbolsExist }.orEmpty() } + + if (filteredEmptySymbols.isNotEmpty() && isNotEmptySymbolsExist) { + val emptyHeader = LoginStudentSelectItem.EmptySymbolsHeader( + isExpanded = isEmptySymbolsExpanded, + onClick = ::onEmptySymbolsToggle, + ) + add(emptyHeader) + if (isEmptySymbolsExpanded) { + filteredEmptySymbols.forEach { + add(createEmptySymbolItem(it)) + } + } + } + + if (filteredEmptySymbols.isNotEmpty() && !isNotEmptySymbolsExist) { + filteredEmptySymbols.forEach { + add(createEmptySymbolItem(it)) + } + } + } + + private fun createEmptySymbolItem(registerSymbol: RegisterSymbol) = + LoginStudentSelectItem.SymbolHeader( + symbol = registerSymbol, + humanReadableName = view?.symbols?.get(registerSymbol.symbol), + isErrorExpanded = expandedSymbolError == registerSymbol, + onClick = ::onSymbolItemClick, + ) + fun onSignIn() { registerStudents(selectedStudents) } - fun onItemSelected(studentWithSemester: StudentWithSemesters, alreadySaved: Boolean) { - if (alreadySaved) return + private fun onEmptySymbolsToggle() { + isEmptySymbolsExpanded = !isEmptySymbolsExpanded + + refreshItems() + } + + private fun onItemSelected(item: LoginStudentSelectItem.Student) { + if (!item.isEnabled) return selectedStudents - .removeAll { it == studentWithSemester } - .let { if (!it) selectedStudents.add(studentWithSemester) } + .removeAll { + it.student.studentId == item.student.studentId && + it.student.classId == item.student.classId && + it.unit.schoolId == item.unit.schoolId && + it.symbol.symbol == item.symbol.symbol + } + .let { if (!it) selectedStudents.add(item) } view?.enableSignIn(selectedStudents.isNotEmpty()) + refreshItems() } - private fun compareStudents(a: Student, b: Student): Boolean { - return a.email == b.email - && a.symbol == b.symbol - && a.studentId == b.studentId - && a.schoolSymbol == b.schoolSymbol - && a.classId == b.classId + private fun onSymbolItemClick(symbol: RegisterSymbol) { + expandedSymbolError = if (symbol != expandedSymbolError) symbol else null + refreshItems() } - private fun loadData(studentsWithSemesters: List) { - resetSelectedState() - - 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) - } - }) - is Resource.Error -> { - errorHandler.dispatch(it.error) - lastError = it.error - view?.updateData(studentsWithSemesters.map { student -> student to false }) - } - } - }.launch() + private fun onUnitItemClick(unit: RegisterUnit) { + expandedSchoolError = if (unit != expandedSchoolError) unit else null + refreshItems() } private fun resetSelectedState() { @@ -89,8 +257,25 @@ class LoginStudentSelectPresenter @Inject constructor( view?.enableSignIn(false) } - private fun registerStudents(studentsWithSemesters: List) { - resourceFlow { studentRepository.saveStudents(studentsWithSemesters) } + private fun refreshItems() { + view?.updateData(createItems()) + } + + private fun registerStudents(students: List) { + val filteredStudents = students.filterIsInstance() + val studentsWithSemesters = filteredStudents.map { item -> + item.student.mapToStudentWithSemesters( + user = registerUser, + symbol = item.symbol, + scrapperDomainSuffix = loginData.domainSuffix, + unit = item.unit, + colors = appInfo.defaultColorsForAvatar, + ) + } + resourceFlow { + studentRepository.saveStudents(studentsWithSemesters) + schoolsRepository.logSchoolLogin(loginData, studentsWithSemesters) + } .logResourceStatus("registration") .onEach { when (it) { @@ -98,16 +283,17 @@ class LoginStudentSelectPresenter @Inject constructor( showProgress(true) showContent(false) } + is Resource.Success -> { syncManager.startOneTimeSyncWorker(quiet = true) - view?.openMainView() + view?.navigateToNext() logRegisterEvent(studentsWithSemesters) } + is Resource.Error -> { view?.apply { showProgress(false) showContent(true) - showContact(true) } lastError = it.error loginErrorHandler.dispatch(it.error) @@ -117,12 +303,23 @@ class LoginStudentSelectPresenter @Inject constructor( }.launch("register") } - fun onDiscordClick() { + private fun onEnterSymbol() { + view?.navigateToSymbol(loginData) + } + + private fun onDiscordClick() { view?.openDiscordInvite() } - fun onEmailClick() { - view?.openEmail(lastError?.message.ifNullOrBlank { "empty" }) + private fun onEmailClick() { + view?.openEmail( + LoginSupportInfo( + loginData = loginData, + registerUser = registerUser, + lastErrorMessage = lastError?.message, + enteredSymbol = loginData.userEnteredSymbol, + ) + ) } private fun logRegisterEvent( diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectView.kt index f2acd76c5..b69700f17 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectView.kt @@ -1,15 +1,20 @@ package io.github.wulkanowy.ui.modules.login.studentselect -import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.ui.base.BaseView +import io.github.wulkanowy.ui.modules.login.LoginData +import io.github.wulkanowy.ui.modules.login.support.LoginSupportInfo interface LoginStudentSelectView : BaseView { + val symbols: Map + fun initView() - fun updateData(data: List>) + fun updateData(data: List) - fun openMainView() + fun navigateToSymbol(loginData: LoginData) + + fun navigateToNext() fun showProgress(show: Boolean) @@ -17,9 +22,7 @@ interface LoginStudentSelectView : BaseView { fun enableSignIn(enable: Boolean) - fun showContact(show: Boolean) - fun openDiscordInvite() - fun openEmail(lastError: String) + fun openEmail(supportInfo: LoginSupportInfo) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/support/LoginSupportDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/support/LoginSupportDialog.kt new file mode 100644 index 000000000..4be2dbaad --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/support/LoginSupportDialog.kt @@ -0,0 +1,149 @@ +package io.github.wulkanowy.ui.modules.login.support + +import android.os.Bundle +import android.util.Patterns +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.os.bundleOf +import androidx.core.widget.doOnTextChanged +import dagger.hilt.android.AndroidEntryPoint +import io.github.wulkanowy.R +import io.github.wulkanowy.data.repositories.PreferencesRepository +import io.github.wulkanowy.databinding.DialogLoginSupportBinding +import io.github.wulkanowy.sdk.scrapper.login.AccountPermissionException +import io.github.wulkanowy.sdk.scrapper.login.InvalidSymbolException +import io.github.wulkanowy.ui.base.BaseDialogFragment +import io.github.wulkanowy.utils.AppInfo +import io.github.wulkanowy.utils.openEmailClient +import io.github.wulkanowy.utils.serializable +import javax.inject.Inject + +@AndroidEntryPoint +class LoginSupportDialog : BaseDialogFragment() { + + @Inject + lateinit var appInfo: AppInfo + + @Inject + lateinit var preferencesRepository: PreferencesRepository + + private lateinit var supportInfo: LoginSupportInfo + + companion object { + private const val ARGUMENT_KEY = "info" + + fun newInstance(info: LoginSupportInfo) = LoginSupportDialog().apply { + arguments = bundleOf(ARGUMENT_KEY to info) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setStyle(STYLE_NO_FRAME, R.style.WulkanowyTheme_NoActionBar) + supportInfo = requireArguments().serializable(ARGUMENT_KEY) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + val binding = DialogLoginSupportBinding.inflate(inflater) + .apply { binding = this } + binding.dialogLoginSupportToolbar.setNavigationOnClickListener { dismiss() } + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + with(binding) { + dialogLoginSupportSchoolInput.doOnTextChanged { _, _, _, _ -> + with(dialogLoginSupportSchoolLayout) { + isErrorEnabled = false + error = null + } + } + dialogLoginSupportSubmit.setOnClickListener { onSubmitClick() } + } + } + + private fun onSubmitClick() { + when { + binding.dialogLoginSupportSchoolInput.text.isNullOrBlank() -> { + with(binding.dialogLoginSupportSchoolLayout) { + isErrorEnabled = true + error = getString(R.string.error_field_required) + } + } + + Patterns.EMAIL_ADDRESS.matcher( + binding.dialogLoginSupportSchoolInput.text.toString() + ).matches() -> { + with(binding.dialogLoginSupportSchoolLayout) { + isErrorEnabled = true + error = getString(R.string.login_support_school_invalid) + } + } + + else -> { + openEmailClientWithFilledTemplate() + dismiss() + } + } + } + + private fun openEmailClientWithFilledTemplate() { + with(binding) { + context?.openEmailClient( + chooserTitle = requireContext().getString(R.string.login_email_intent_title), + email = "wulkanowyinc@gmail.com", + subject = requireContext().getString(R.string.login_email_subject), + body = requireContext().getString( + R.string.login_email_text, + "${appInfo.systemManufacturer} ${appInfo.systemModel}", + appInfo.systemVersion.toString(), + "${appInfo.versionName}-${appInfo.buildFlavor}", + supportInfo.loginData.let { "${it.baseUrl}/${it.defaultSymbol}/${it.userEnteredSymbol}" }, + preferencesRepository.installationId, + getLastErrorFromStudentSelectScreen(), + dialogLoginSupportSchoolInput.text.takeIf { !it.isNullOrBlank() } + ?: return@with, + dialogLoginSupportAdditionalInput.text, + ) + ) + } + } + + private fun getLastErrorFromStudentSelectScreen(): String { + if (!supportInfo.lastErrorMessage.isNullOrBlank()) { + return supportInfo.lastErrorMessage!! + } + if (supportInfo.registerUser?.symbols.isNullOrEmpty()) { + return "" + } + + return "\n" + supportInfo.registerUser?.symbols?.filterNot { + (it.error is AccountPermissionException || it.error is InvalidSymbolException) && + it.symbol != supportInfo.enteredSymbol + }?.joinToString(";\n") { symbol -> + buildString { + append(" -") + append(symbol.symbol) + append("(${symbol.error?.message?.let { it.take(46) + "..." } ?: symbol.schools.size})") + if (symbol.schools.isNotEmpty()) { + append(": ") + } + append(symbol.schools.joinToString(", ") { unit -> + buildString { + append(unit.schoolShortName) + append("(${unit.error?.message?.let { it.take(46) + "..." } ?: unit.students.size})") + } + }) + } + } + "\nPozostałe: " + supportInfo.registerUser?.symbols?.filter { + it.error is AccountPermissionException || it.error is InvalidSymbolException + }?.joinToString(", ") { it.symbol } + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/support/LoginSupportInfo.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/support/LoginSupportInfo.kt new file mode 100644 index 000000000..3f101dd63 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/support/LoginSupportInfo.kt @@ -0,0 +1,12 @@ +package io.github.wulkanowy.ui.modules.login.support + +import io.github.wulkanowy.data.pojos.RegisterUser +import io.github.wulkanowy.ui.modules.login.LoginData +import java.io.Serializable + +data class LoginSupportInfo( + val loginData: LoginData, + val registerUser: RegisterUser?, + val lastErrorMessage: String?, + val enteredSymbol: String?, +) : Serializable 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 58bdf6cef..23ebffe9d 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 @@ -12,15 +12,18 @@ import androidx.core.text.parseAsHtml import androidx.core.widget.doOnTextChanged import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R -import io.github.wulkanowy.data.db.entities.StudentWithSemesters +import io.github.wulkanowy.data.pojos.RegisterUser +import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.databinding.FragmentLoginSymbolBinding import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.modules.login.LoginActivity import io.github.wulkanowy.ui.modules.login.LoginData +import io.github.wulkanowy.ui.modules.login.support.LoginSupportDialog +import io.github.wulkanowy.ui.modules.login.support.LoginSupportInfo import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.hideSoftInput -import io.github.wulkanowy.utils.openEmailClient import io.github.wulkanowy.utils.openInternetBrowser +import io.github.wulkanowy.utils.serializable import io.github.wulkanowy.utils.showSoftInput import javax.inject.Inject @@ -34,6 +37,9 @@ class LoginSymbolFragment : @Inject lateinit var appInfo: AppInfo + @Inject + lateinit var preferencesRepository: PreferencesRepository + companion object { private const val SAVED_LOGIN_DATA = "LOGIN_DATA" @@ -42,6 +48,8 @@ class LoginSymbolFragment : } } + override val symbolValue: String? get() = binding.loginSymbolName.text?.toString() + override val symbolNameError: CharSequence? get() = binding.loginSymbolNameLayout.error @@ -50,7 +58,7 @@ class LoginSymbolFragment : binding = FragmentLoginSymbolBinding.bind(view) presenter.onAttachView( view = this, - loginData = requireArguments().getSerializable(SAVED_LOGIN_DATA) as LoginData, + loginData = requireArguments().serializable(SAVED_LOGIN_DATA), ) } @@ -58,7 +66,7 @@ class LoginSymbolFragment : (requireActivity() as LoginActivity).showActionBar(true) with(binding) { - loginSymbolSignIn.setOnClickListener { presenter.attemptLogin(loginSymbolName.text.toString()) } + loginSymbolSignIn.setOnClickListener { presenter.attemptLogin() } loginSymbolFaq.setOnClickListener { presenter.onFaqClick() } loginSymbolContactEmail.setOnClickListener { presenter.onEmailClick() } @@ -91,10 +99,28 @@ class LoginSymbolFragment : } } - override fun setErrorSymbolRequire() { - binding.loginSymbolNameLayout.apply { + override fun setErrorSymbolInvalid() { + with(binding.loginSymbolNameLayout) { requestFocus() - error = getString(R.string.error_field_required) + error = getString(R.string.login_invalid_symbol) + } + } + + override fun setErrorSymbolDefinitelyInvalid() { + with(binding.loginSymbolNameLayout) { + requestFocus() + error = getString(R.string.login_invalid_symbol_definitely) + } + } + + override fun setErrorSymbolRequire() { + setErrorSymbol(getString(R.string.error_field_required)) + } + + override fun setErrorSymbol(message: String) { + with(binding.loginSymbolNameLayout) { + requestFocus() + error = message } } @@ -125,8 +151,8 @@ class LoginSymbolFragment : binding.loginSymbolContainer.visibility = if (show) VISIBLE else GONE } - override fun navigateToStudentSelect(studentsWithSemesters: List) { - (activity as? LoginActivity)?.navigateToStudentSelect(studentsWithSemesters) + override fun navigateToStudentSelect(loginData: LoginData, registerUser: RegisterUser) { + (activity as? LoginActivity)?.navigateToStudentSelect(loginData, registerUser) } override fun onSaveInstanceState(outState: Bundle) { @@ -150,19 +176,7 @@ class LoginSymbolFragment : ) } - override fun openEmail(host: String, lastError: String) { - context?.openEmailClient( - chooserTitle = requireContext().getString(R.string.login_email_intent_title), - email = "wulkanowyinc@gmail.com", - subject = requireContext().getString(R.string.login_email_subject), - body = requireContext().getString( - R.string.login_email_text, - "${appInfo.systemManufacturer} ${appInfo.systemModel}", - appInfo.systemVersion.toString(), - appInfo.versionName, - "$host/${binding.loginSymbolName.text}", - lastError - ) - ) + override fun openSupportDialog(supportInfo: LoginSupportInfo) { + LoginSupportDialog.newInstance(supportInfo).show(childFragmentManager, "support_dialog") } } 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 691cd4481..5c31f14d4 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,12 +1,17 @@ package io.github.wulkanowy.ui.modules.login.symbol import io.github.wulkanowy.data.Resource +import io.github.wulkanowy.data.dataOrNull import io.github.wulkanowy.data.onResourceNotLoading +import io.github.wulkanowy.data.pojos.RegisterUser import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.resourceFlow +import io.github.wulkanowy.sdk.scrapper.getNormalizedSymbol +import io.github.wulkanowy.sdk.scrapper.login.InvalidSymbolException import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.modules.login.LoginData import io.github.wulkanowy.ui.modules.login.LoginErrorHandler +import io.github.wulkanowy.ui.modules.login.support.LoginSupportInfo import io.github.wulkanowy.utils.AnalyticsHelper import io.github.wulkanowy.utils.ifNullOrBlank import kotlinx.coroutines.flow.onEach @@ -23,9 +28,14 @@ class LoginSymbolPresenter @Inject constructor( lateinit var loginData: LoginData + private var registerUser: RegisterUser? = null + fun onAttachView(view: LoginSymbolView, loginData: LoginData) { super.onAttachView(view) this.loginData = loginData + loginErrorHandler.onBadCredentials = { + view.setErrorSymbol(it.orEmpty()) + } with(view) { initView() showContact(false) @@ -39,29 +49,39 @@ class LoginSymbolPresenter @Inject constructor( view?.apply { if (symbolNameError != null) clearSymbolError() } } - fun attemptLogin(symbol: String) { - if (symbol.isBlank()) { + fun attemptLogin() { + if (view?.symbolValue.isNullOrBlank()) { view?.setErrorSymbolRequire() return } + if (isFormDefinitelyInvalid()) { + view?.setErrorSymbolDefinitelyInvalid() + return + } + loginData = loginData.copy( + userEnteredSymbol = view?.symbolValue?.getNormalizedSymbol(), + ) resourceFlow { - studentRepository.getStudentsScrapper( + studentRepository.getUserSubjectsFromScrapper( email = loginData.login, password = loginData.password, scrapperBaseUrl = loginData.baseUrl, - symbol = symbol, + domainSuffix = loginData.domainSuffix, + symbol = loginData.userEnteredSymbol.orEmpty(), ) - }.onEach { - when (it) { + }.onEach { user -> + registerUser = user.dataOrNull + when (user) { is Resource.Loading -> view?.run { Timber.i("Login with symbol started") hideSoftKeyboard() showProgress(true) showContent(false) } + is Resource.Success -> { - when (it.data.size) { + when (user.data.symbols.size) { 0 -> { Timber.i("Login with symbol result: Empty student list") view?.run { @@ -69,20 +89,29 @@ class LoginSymbolPresenter @Inject constructor( showContact(true) } } + else -> { - Timber.i("Login with symbol result: Success") - view?.navigateToStudentSelect(requireNotNull(it.data)) + val enteredSymbolDetails = user.data.symbols + .firstOrNull() + ?.takeIf { it.symbol == loginData.userEnteredSymbol } + + if (enteredSymbolDetails?.error is InvalidSymbolException) { + showInvalidSymbolError() + } else { + Timber.i("Login with symbol result: Success") + view?.navigateToStudentSelect(loginData, requireNotNull(user.data)) + } } } analytics.logEvent( "registration_symbol", "success" to true, - "students" to it.data.size, "scrapperBaseUrl" to loginData.baseUrl, - "symbol" to symbol, + "symbol" to view?.symbolValue, "error" to "No error" ) } + is Resource.Error -> { Timber.i("Login with symbol result: An exception occurred") analytics.logEvent( @@ -90,12 +119,15 @@ class LoginSymbolPresenter @Inject constructor( "success" to false, "students" to -1, "scrapperBaseUrl" to loginData.baseUrl, - "symbol" to symbol, - "error" to it.error.message.ifNullOrBlank { "No message" } + "symbol" to view?.symbolValue, + "error" to user.error.message.ifNullOrBlank { "No message" } ) - loginErrorHandler.dispatch(it.error) - lastError = it.error + loginErrorHandler.dispatch(user.error) + lastError = user.error view?.showContact(true) + if (user.error is InvalidSymbolException) { + showInvalidSymbolError() + } } } }.onResourceNotLoading { @@ -106,11 +138,32 @@ class LoginSymbolPresenter @Inject constructor( }.launch("login") } + private fun isFormDefinitelyInvalid(): Boolean { + val definitelyInvalidSymbols = listOf("vulcan", "uonet", "wulkanowy", "standardowa") + val normalizedSymbol = view?.symbolValue.orEmpty().getNormalizedSymbol() + + return normalizedSymbol in definitelyInvalidSymbols + } + + private fun showInvalidSymbolError() { + view?.run { + setErrorSymbolInvalid() + showContact(true) + } + } + fun onFaqClick() { view?.openFaqPage() } fun onEmailClick() { - view?.openEmail(loginData.baseUrl, lastError?.message.ifNullOrBlank { "empty" }) + view?.openSupportDialog( + LoginSupportInfo( + loginData = loginData, + registerUser = registerUser, + lastErrorMessage = lastError?.message, + enteredSymbol = view?.symbolValue, + ) + ) } } 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 527895b77..ace12f780 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolView.kt @@ -1,10 +1,14 @@ package io.github.wulkanowy.ui.modules.login.symbol -import io.github.wulkanowy.data.db.entities.StudentWithSemesters +import io.github.wulkanowy.data.pojos.RegisterUser import io.github.wulkanowy.ui.base.BaseView +import io.github.wulkanowy.ui.modules.login.LoginData +import io.github.wulkanowy.ui.modules.login.support.LoginSupportInfo interface LoginSymbolView : BaseView { + val symbolValue: String? + val symbolNameError: CharSequence? fun initView() @@ -13,8 +17,14 @@ interface LoginSymbolView : BaseView { fun setErrorSymbolIncorrect() + fun setErrorSymbolInvalid() + + fun setErrorSymbolDefinitelyInvalid() + fun setErrorSymbolRequire() + fun setErrorSymbol(message: String) + fun clearSymbolError() fun clearAndFocusSymbol() @@ -27,11 +37,11 @@ interface LoginSymbolView : BaseView { fun showContent(show: Boolean) - fun navigateToStudentSelect(studentsWithSemesters: List) + fun navigateToStudentSelect(loginData: LoginData, registerUser: RegisterUser) fun showContact(show: Boolean) fun openFaqPage() - fun openEmail(host: String, lastError: String) + fun openSupportDialog(supportInfo: LoginSupportInfo) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/LuckyNumberFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/LuckyNumberFragment.kt index 0a73fe15d..a6c75c1d9 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/LuckyNumberFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/LuckyNumberFragment.kt @@ -18,7 +18,7 @@ import javax.inject.Inject @AndroidEntryPoint class LuckyNumberFragment : BaseFragment(R.layout.fragment_lucky_number), LuckyNumberView, - MainView.TitledView { + MainView.TitledView, MainView.MainChildView { @Inject lateinit var presenter: LuckyNumberPresenter @@ -86,6 +86,14 @@ class LuckyNumberFragment : (activity as? MainActivity)?.pushView(LuckyNumberHistoryFragment.newInstance()) } + override fun onFragmentReselected() { + if (::presenter.isInitialized) presenter.onViewReselected() + } + + override fun popView() { + (activity as? MainActivity)?.popView() + } + override fun onDestroyView() { presenter.onDetachView() super.onDestroyView() 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 6f5c8e740..2039624b1 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 @@ -99,4 +99,9 @@ class LuckyNumberPresenter @Inject constructor( fun onDetailsClick() { view?.showErrorDetailsDialog(lastError) } + + fun onViewReselected() { + Timber.i("Luckynumber view is reselected") + view?.popView() + } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/LuckyNumberView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/LuckyNumberView.kt index 0c05a1566..3d34ebc8f 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/LuckyNumberView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/LuckyNumberView.kt @@ -26,4 +26,6 @@ interface LuckyNumberView : BaseView { fun showContent(show: Boolean) fun openLuckyNumberHistory() + + fun popView() } 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 53f06cacd..a78ce5dd2 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 @@ -61,7 +61,7 @@ class LuckyNumberHistoryFragment : luckyNumberHistoryPreviousButton.setOnClickListener { presenter.onPreviousWeek() } luckyNumberHistoryNextButton.setOnClickListener { presenter.onNextWeek() } - luckyNumberHistoryNavContainer.elevation = requireContext().dpToPx(8f) + luckyNumberHistoryNavContainer.elevation = requireContext().dpToPx(3f) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetConfigureActivity.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetConfigureActivity.kt index 024beff85..a2d23e543 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetConfigureActivity.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetConfigureActivity.kt @@ -1,16 +1,12 @@ package io.github.wulkanowy.ui.modules.luckynumberwidget -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 import android.widget.Toast import androidx.appcompat.app.AlertDialog 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.ActivityWidgetConfigureBinding import io.github.wulkanowy.ui.base.BaseActivity @@ -41,7 +37,6 @@ class LuckyNumberWidgetConfigureActivity : setContentView( ActivityWidgetConfigureBinding.inflate(layoutInflater).apply { binding = this }.root ) - intent.extras.let { presenter.onAttachView(this, it?.getInt(EXTRA_APPWIDGET_ID)) } @@ -56,22 +51,6 @@ class LuckyNumberWidgetConfigureActivity : configureAdapter.onClickListener = presenter::onItemSelect } - override fun showThemeDialog() { - var items = arrayOf( - getString(R.string.widget_timetable_theme_light), - getString(R.string.widget_timetable_theme_dark) - ) - if (appInfo.systemVersion >= Build.VERSION_CODES.Q) items += (getString(R.string.widget_timetable_theme_system)) - - dialog = AlertDialog.Builder(this, R.style.WulkanowyTheme_WidgetAccountSwitcher) - .setTitle(R.string.widget_timetable_theme_title) - .setOnDismissListener { presenter.onDismissThemeView() } - .setSingleChoiceItems(items, -1) { _, which -> - presenter.onThemeSelect(which) - } - .show() - } - override fun updateData(data: List, selectedStudentId: Long) { with(configureAdapter) { selectedId = selectedStudentId 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 cac648da8..7e53dad06 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 @@ -8,7 +8,6 @@ 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 kotlinx.coroutines.flow.onEach import timber.log.Timber import javax.inject.Inject @@ -32,20 +31,9 @@ class LuckyNumberWidgetConfigurePresenter @Inject constructor( fun onItemSelect(student: Student) { selectedStudent = student - view?.showThemeDialog() - } - - fun onThemeSelect(index: Int) { - appWidgetId?.let { - sharedPref.putLong(getThemeWidgetKey(it), index.toLong()) - } registerStudent(selectedStudent) } - fun onDismissThemeView() { - view?.finishView() - } - private fun loadData() { resourceFlow { studentRepository.getSavedStudents(false) }.onEach { when (it) { @@ -56,10 +44,7 @@ class LuckyNumberWidgetConfigurePresenter @Inject constructor( } ?: -1 when { it.data.isEmpty() -> view?.openLoginView() - it.data.size == 1 -> { - selectedStudent = it.data.single().student - view?.showThemeDialog() - } + it.data.size == 1 -> onItemSelect(it.data.single().student) else -> view?.updateData(it.data, selectedStudentId) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetConfigureView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetConfigureView.kt index b4556f7ef..df13b993d 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetConfigureView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetConfigureView.kt @@ -7,8 +7,6 @@ interface LuckyNumberWidgetConfigureView : BaseView { fun initView() - fun showThemeDialog() - fun updateData(data: List, selectedStudentId: Long) fun updateLuckyNumberWidget(widgetId: Int) 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 e03e3e90e..bafb2d7e5 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 @@ -2,14 +2,12 @@ package io.github.wulkanowy.ui.modules.luckynumberwidget import android.app.PendingIntent import android.appwidget.AppWidgetManager -import android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT -import android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH import android.appwidget.AppWidgetProvider import android.content.Context import android.content.res.Configuration import android.os.Bundle -import android.view.View.GONE -import android.view.View.VISIBLE +import android.util.TypedValue.COMPLEX_UNIT_SP +import android.view.View import android.widget.RemoteViews import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R @@ -17,7 +15,6 @@ 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 @@ -41,16 +38,12 @@ class LuckyNumberWidgetProvider : AppWidgetProvider() { lateinit var sharedPref: SharedPrefProvider companion object { + private const val LUCKY_NUMBER_WIDGET_MAX_SIZE = 196 - const val LUCKY_NUMBER_PENDING_INTENT_ID = 200 + private const val LUCKY_NUMBER_PENDING_INTENT_ID = 300 + private const val LUCKY_NUMBER_HISTORY_PENDING_INTENT_ID = 301 fun getStudentWidgetKey(appWidgetId: Int) = "lucky_number_widget_student_$appWidgetId" - - fun getThemeWidgetKey(appWidgetId: Int) = "lucky_number_widget_theme_$appWidgetId" - - fun getHeightWidgetKey(appWidgetId: Int) = "lucky_number_widget_height_$appWidgetId" - - fun getWidthWidgetKey(appWidgetId: Int) = "lucky_number_widget_width_$appWidgetId" } override fun onUpdate( @@ -59,107 +52,86 @@ class LuckyNumberWidgetProvider : AppWidgetProvider() { appWidgetIds: IntArray? ) { super.onUpdate(context, appWidgetManager, appWidgetIds) - appWidgetIds?.forEach { appWidgetId -> - val luckyNumber = - getLuckyNumber(sharedPref.getLong(getStudentWidgetKey(appWidgetId), 0), appWidgetId) - val appIntent = PendingIntent.getActivity( - context, - LUCKY_NUMBER_PENDING_INTENT_ID, - SplashActivity.getStartIntent(context, Destination.LuckyNumber), - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE - ) - if (luckyNumber is Resource.Error) { - Timber.e("Error loading lucky number for widget", luckyNumber.error) - } + val appIntent = PendingIntent.getActivity( + context, + LUCKY_NUMBER_PENDING_INTENT_ID, + SplashActivity.getStartIntent(context, Destination.LuckyNumber), + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE + ) - val remoteView = - RemoteViews(context.packageName, getCorrectLayoutId(appWidgetId, context)) - .apply { - setTextViewText( - R.id.luckyNumberWidgetNumber, - luckyNumber.dataOrNull?.luckyNumber?.toString() ?: "#" - ) - setOnClickPendingIntent(R.id.luckyNumberWidgetContainer, appIntent) - } + val historyIntent = PendingIntent.getActivity( + context, + LUCKY_NUMBER_HISTORY_PENDING_INTENT_ID, + SplashActivity.getStartIntent(context, Destination.LuckyNumberHistory), + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE + ) - setStyles(remoteView, appWidgetId) - appWidgetManager.updateAppWidget(appWidgetId, remoteView) + appWidgetIds?.forEach { widgetId -> + val studentId = sharedPref.getLong(getStudentWidgetKey(widgetId), 0) + val luckyNumberResource = getLuckyNumber(studentId, widgetId) + val luckyNumber = luckyNumberResource.dataOrNull?.luckyNumber?.toString() + val remoteView = RemoteViews(context.packageName, R.layout.widget_luckynumber) + .apply { + setTextViewText(R.id.luckyNumberWidgetValue, luckyNumber ?: "-") + setOnClickPendingIntent(R.id.luckyNumberWidgetContainer, appIntent) + setOnClickPendingIntent(R.id.luckyNumberWidgetHistoryButton, historyIntent) + } + + resizeWidget(context, appWidgetManager.getAppWidgetOptions(widgetId), remoteView) + appWidgetManager.updateAppWidget(widgetId, remoteView) + } + } + + override fun onAppWidgetOptionsChanged( + context: Context?, + appWidgetManager: AppWidgetManager?, + appWidgetId: Int, + newOptions: Bundle? + ) { + super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions) + + if (context == null || newOptions == null || appWidgetManager == null) { + return + } + + val remoteView = RemoteViews(context.packageName, R.layout.widget_luckynumber) + resizeWidget(context, newOptions, remoteView) + appWidgetManager.partiallyUpdateAppWidget(appWidgetId, remoteView) + } + + private fun resizeWidget(context: Context, options: Bundle, remoteViews: RemoteViews) { + val (width, height) = options.getWidgetSize(context) + val size = minOf(width, height, LUCKY_NUMBER_WIDGET_MAX_SIZE).toFloat() + resizeWidgetContents(size, remoteViews) + Timber.v("LuckyNumberWidget resized: ${width}x${height} ($size)") + } + + private fun resizeWidgetContents(size: Float, remoteViews: RemoteViews) { + var historyButtonVisibility = View.VISIBLE + var luckyNumberTextSize = 72f + + if (size < 150) { + luckyNumberTextSize = 44f + historyButtonVisibility = View.GONE + } + if (size < 75) { + luckyNumberTextSize = 26f + } + + remoteViews.apply { + setTextViewTextSize(R.id.luckyNumberWidgetValue, COMPLEX_UNIT_SP, luckyNumberTextSize) + setViewVisibility(R.id.luckyNumberWidgetHistoryButton, historyButtonVisibility) } } override fun onDeleted(context: Context?, appWidgetIds: IntArray?) { super.onDeleted(context, appWidgetIds) appWidgetIds?.forEach { appWidgetId -> - with(sharedPref) { - delete(getHeightWidgetKey(appWidgetId)) - delete(getStudentWidgetKey(appWidgetId)) - delete(getThemeWidgetKey(appWidgetId)) - delete(getWidthWidgetKey(appWidgetId)) - } + sharedPref.delete(getStudentWidgetKey(appWidgetId)) } } - override fun onAppWidgetOptionsChanged( - context: Context, - appWidgetManager: AppWidgetManager, - appWidgetId: Int, - newOptions: Bundle? - ) { - super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions) - - val remoteView = RemoteViews(context.packageName, getCorrectLayoutId(appWidgetId, context)) - - setStyles(remoteView, appWidgetId, newOptions) - appWidgetManager.updateAppWidget(appWidgetId, remoteView) - } - - private fun setStyles(views: RemoteViews, appWidgetId: Int, options: Bundle? = null) { - val width = options?.getInt(OPTION_APPWIDGET_MIN_WIDTH) ?: sharedPref.getLong( - getWidthWidgetKey(appWidgetId), 74 - ).toInt() - val height = options?.getInt(OPTION_APPWIDGET_MAX_HEIGHT) ?: sharedPref.getLong( - getHeightWidgetKey(appWidgetId), 74 - ).toInt() - - with(sharedPref) { - putLong(getWidthWidgetKey(appWidgetId), width.toLong()) - putLong(getHeightWidgetKey(appWidgetId), height.toLong()) - } - - val rows = getCellsForSize(height) - val cols = getCellsForSize(width) - - Timber.d("New lucky number widget measurement: %dx%d", width, height) - Timber.d("Widget size: $cols x $rows") - - when { - 1 == cols && 1 == rows -> views.setVisibility(imageTop = false, imageLeft = false) - 1 == cols && 1 < rows -> views.setVisibility(imageTop = true, imageLeft = false) - 1 < cols && 1 == rows -> views.setVisibility(imageTop = false, imageLeft = true) - 1 == cols && 1 == rows -> views.setVisibility(imageTop = true, imageLeft = false) - 2 == cols && 1 == rows -> views.setVisibility(imageTop = false, imageLeft = true) - else -> views.setVisibility(imageTop = false, imageLeft = false, title = true) - } - } - - private fun RemoteViews.setVisibility( - imageTop: Boolean, - imageLeft: Boolean, - title: Boolean = false - ) { - setViewVisibility(R.id.luckyNumberWidgetImageTop, if (imageTop) VISIBLE else GONE) - setViewVisibility(R.id.luckyNumberWidgetImageLeft, if (imageLeft) VISIBLE else GONE) - setViewVisibility(R.id.luckyNumberWidgetTitle, if (title) VISIBLE else GONE) - setViewVisibility(R.id.luckyNumberWidgetNumber, VISIBLE) - } - - private fun getCellsForSize(size: Int): Int { - var n = 2 - while (74 * n - 30 < size) ++n - return n - 1 - } - private fun getLuckyNumber(studentId: Long, appWidgetId: Int) = runBlocking { try { val students = studentRepository.getSavedStudents() @@ -181,22 +153,24 @@ class LuckyNumberWidgetProvider : AppWidgetProvider() { Resource.Success(null) } } catch (e: Exception) { - if (e.cause !is NoCurrentStudentException) { - Timber.e(e, "An error has occurred in lucky number provider") - } + Timber.e(e, "An error has occurred in lucky number provider") Resource.Error(e) } } - private fun getCorrectLayoutId(appWidgetId: Int, context: Context): Int { - val savedTheme = sharedPref.getLong(getThemeWidgetKey(appWidgetId), 0) - val isSystemDarkMode = - context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES + private fun Bundle.getWidgetSize(context: Context): Pair { + val minWidth = getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH) + val maxWidth = getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH) + val minHeight = getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT) + val maxHeight = getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT) - return if (savedTheme == 1L || (savedTheme == 2L && isSystemDarkMode)) { - R.layout.widget_luckynumber_dark + val orientation = context.resources.configuration.orientation + val isPortrait = orientation == Configuration.ORIENTATION_PORTRAIT + + return if (isPortrait) { + minWidth to maxHeight } else { - R.layout.widget_luckynumber + maxWidth to minHeight } } } 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 5cd6fa103..62c16257e 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 @@ -2,14 +2,21 @@ package io.github.wulkanowy.ui.modules.main import android.content.Context import android.content.Intent -import android.os.Build.VERSION_CODES.P +import android.os.Build import android.os.Bundle import android.view.Menu import android.view.MenuItem +import android.view.ViewGroup.MarginLayoutParams +import androidx.activity.OnBackPressedCallback +import androidx.activity.addCallback import androidx.core.view.ViewCompat +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat import androidx.core.view.isVisible +import androidx.core.view.updateLayoutParams import androidx.fragment.app.DialogFragment import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import com.google.android.material.dialog.MaterialAlertDialogBuilder @@ -21,15 +28,32 @@ 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.* +import io.github.wulkanowy.ui.modules.auth.AuthDialog +import io.github.wulkanowy.ui.modules.captcha.CaptchaDialog +import io.github.wulkanowy.ui.modules.settings.appearance.menuorder.AppMenuItem +import io.github.wulkanowy.utils.AnalyticsHelper +import io.github.wulkanowy.utils.AppInfo +import io.github.wulkanowy.utils.InAppReviewHelper +import io.github.wulkanowy.utils.InAppUpdateHelper +import io.github.wulkanowy.utils.createNameInitialsDrawable +import io.github.wulkanowy.utils.dpToPx +import io.github.wulkanowy.utils.nickOrName +import io.github.wulkanowy.utils.safelyPopFragments +import io.github.wulkanowy.utils.setOnViewChangeListener +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import timber.log.Timber import javax.inject.Inject +import kotlin.time.Duration.Companion.seconds @AndroidEntryPoint class MainActivity : BaseActivity(), MainView, @@ -42,7 +66,7 @@ class MainActivity : BaseActivity(), MainVie lateinit var analytics: AnalyticsHelper @Inject - lateinit var updateHelper: UpdateHelper + lateinit var inAppUpdateHelper: InAppUpdateHelper @Inject lateinit var inAppReviewHelper: InAppReviewHelper @@ -50,6 +74,8 @@ class MainActivity : BaseActivity(), MainVie @Inject lateinit var appInfo: AppInfo + private var onBackCallback: OnBackPressedCallback? = null + private var accountMenu: MenuItem? = null private val overlayProvider by lazy { ElevationOverlayProvider(this) } @@ -57,6 +83,8 @@ class MainActivity : BaseActivity(), MainVie private val navController = FragNavController(supportFragmentManager, R.id.main_fragment_container) + private val captchaVerificationEvent = MutableSharedFlow() + companion object { private const val EXTRA_START_DESTINATION = "start_destination_json" @@ -85,28 +113,31 @@ class MainActivity : BaseActivity(), MainVie super.onCreate(savedInstanceState) setContentView(ActivityMainBinding.inflate(layoutInflater).apply { binding = this }.root) setSupportActionBar(binding.mainToolbar) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + WindowCompat.setDecorFitsSystemWindows(window, false) + binding.mainAppBar.isLifted = true + } + initializeFragmentContainer() + this.savedInstanceState = savedInstanceState messageContainer = binding.mainMessageContainer - updateHelper.messageContainer = binding.mainFragmentContainer + messageAnchor = binding.mainMessageContainer + inAppUpdateHelper.messageContainer = binding.mainFragmentContainer + onBackCallback = onBackPressedDispatcher.addCallback(this, enabled = false) { + presenter.onBackPressed() + } val destination = intent.getStringExtra(EXTRA_START_DESTINATION) ?.takeIf { savedInstanceState == null } presenter.onAttachView(this, destination) - updateHelper.checkAndInstallUpdates(this) + inAppUpdateHelper.checkAndInstallUpdates() } 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) + inAppUpdateHelper.onResume() } override fun onCreateOptionsMenu(menu: Menu): Boolean { @@ -117,13 +148,21 @@ class MainActivity : BaseActivity(), MainVie return true } - override fun initView(startMenuIndex: Int, rootDestinations: List) { + override fun initView( + startMenuIndex: Int, + rootAppMenuItems: List, + rootUpdatedDestinations: List + ) { initializeToolbar() - initializeBottomNavigation(startMenuIndex) - initializeNavController(startMenuIndex, rootDestinations) + initializeBottomNavigation(startMenuIndex, rootAppMenuItems) + initializeNavController(startMenuIndex, rootUpdatedDestinations) + initializeCaptchaVerificationEvent() } - private fun initializeNavController(startMenuIndex: Int, rootDestinations: List) { + private fun initializeNavController( + startMenuIndex: Int, + rootUpdatedDestinations: List + ) { with(navController) { setOnViewChangeListener { destinationView -> presenter.onViewChange(destinationView) @@ -133,7 +172,7 @@ class MainActivity : BaseActivity(), MainVie ) } fragmentHideStrategy = HIDE - rootFragments = rootDestinations.map { it.destinationFragment } + rootFragments = rootUpdatedDestinations.map { it.destinationFragment } initialize(startMenuIndex, savedInstanceState) } @@ -149,17 +188,16 @@ class MainActivity : BaseActivity(), MainVie } } - private fun initializeBottomNavigation(startMenuIndex: Int) { + private fun initializeBottomNavigation( + startMenuIndex: Int, + rootAppMenuItems: List + ) { with(binding.mainBottomNav) { with(menu) { - add(Menu.NONE, 0, Menu.NONE, R.string.dashboard_title) - .setIcon(R.drawable.ic_main_dashboard) - add(Menu.NONE, 1, Menu.NONE, R.string.grade_title) - .setIcon(R.drawable.ic_main_grade) - add(Menu.NONE, 2, Menu.NONE, R.string.attendance_title) - .setIcon(R.drawable.ic_main_attendance) - add(Menu.NONE, 3, Menu.NONE, R.string.timetable_title) - .setIcon(R.drawable.ic_main_timetable) + rootAppMenuItems.forEachIndexed { index, item -> + add(Menu.NONE, index, Menu.NONE, item.title) + .setIcon(item.icon) + } add(Menu.NONE, 4, Menu.NONE, R.string.more_title) .setIcon(R.drawable.ic_main_more) } @@ -173,6 +211,17 @@ class MainActivity : BaseActivity(), MainVie } } + private fun initializeFragmentContainer() { + ViewCompat.setOnApplyWindowInsetsListener(binding.mainFragmentContainer) { view, insets -> + val bottomInsets = insets.getInsets(WindowInsetsCompat.Type.navigationBars()) + + view.updateLayoutParams { + bottomMargin = if (binding.mainBottomNav.isVisible) 0 else bottomInsets.bottom + } + WindowInsetsCompat.CONSUMED + } + } + override fun onPreferenceStartFragment( caller: PreferenceFragmentCompat, pref: Preference @@ -217,20 +266,9 @@ class MainActivity : BaseActivity(), MainVie showDialogFragment(AccountQuickDialog.newInstance(studentWithSemesters)) } - override fun showActionBarElevation(show: Boolean) { - ViewCompat.setElevation(binding.mainToolbar, if (show) dpToPx(4f) else 0f) - } - override fun showBottomNavigation(show: Boolean) { binding.mainBottomNav.isVisible = show - - if (appInfo.systemVersion >= P) { - window.navigationBarColor = if (show) { - getThemeAttrColor(android.R.attr.navigationBarColor) - } else { - getThemeAttrColor(R.attr.colorSurface) - } - } + binding.mainFragmentContainer.requestApplyInsets() } override fun openMoreDestination(destination: Destination) { @@ -266,6 +304,7 @@ class MainActivity : BaseActivity(), MainVie analytics.popCurrentScreen(navController.currentFrag!!::class.simpleName) navController.pushFragment(fragment) + onBackCallback?.isEnabled = !isRootView } override fun popView(depth: Int) { @@ -273,10 +312,7 @@ class MainActivity : BaseActivity(), MainVie analytics.popCurrentScreen(navController.currentFrag!!::class.simpleName) navController.safelyPopFragments(depth) - } - - override fun onBackPressed() { - presenter.onBackPressed { super.onBackPressed() } + onBackCallback?.isEnabled = !isRootView } override fun showStudentAvatar(student: Student) { @@ -300,38 +336,25 @@ class MainActivity : BaseActivity(), MainVie .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() } + @OptIn(FlowPreview::class) + private fun initializeCaptchaVerificationEvent() { + captchaVerificationEvent + .debounce(1.seconds) + .onEach { url -> + Timber.d("Showing captcha dialog for: $url") + showDialogFragment(CaptchaDialog.newInstance(url)) + } + .launchIn(lifecycleScope) } - override fun openPrivacyPolicy() { - openInternetBrowser( - "https://wulkanowy.github.io/polityka-prywatnosci.html", - ::showMessage - ) + override fun onCaptchaVerificationRequired(url: String?) { + lifecycleScope.launch { + captchaVerificationEvent.emit(url) + } + } + + override fun showAuthDialog() { + showDialogFragment(AuthDialog.newInstance()) } override fun onSaveInstanceState(outState: Bundle) { 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 9c32d8583..5469fcad3 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 @@ -14,15 +14,11 @@ import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.ui.modules.Destination import io.github.wulkanowy.ui.modules.account.AccountView import io.github.wulkanowy.ui.modules.account.accountdetails.AccountDetailsView -import io.github.wulkanowy.ui.modules.grade.GradeView -import io.github.wulkanowy.ui.modules.message.MessageView -import io.github.wulkanowy.ui.modules.schoolandteachers.SchoolAndTeachersView import io.github.wulkanowy.ui.modules.studentinfo.StudentInfoView import io.github.wulkanowy.utils.AdsHelper import io.github.wulkanowy.utils.AnalyticsHelper 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.Duration @@ -42,20 +38,20 @@ class MainPresenter @Inject constructor( private var studentsWitSemesters: List? = null - private val rootDestinationTypeList = listOf( - Destination.Type.DASHBOARD, - Destination.Type.GRADE, - Destination.Type.ATTENDANCE, - Destination.Type.TIMETABLE, - Destination.Type.MORE - ) + private val rootAppMenuItems = preferencesRepository.appMenuItemOrder + .sortedBy { it.order } + .take(4) + + private val rootDestinationTypeList = rootAppMenuItems.map { it.destinationType } + .plus(Destination.Type.MORE) private val Destination?.startMenuIndex get() = when { - this == null -> preferencesRepository.startMenuIndex + this == null -> 0 destinationType in rootDestinationTypeList -> { rootDestinationTypeList.indexOf(destinationType) } + else -> 4 } @@ -69,7 +65,7 @@ class MainPresenter @Inject constructor( if (it == initDestination?.destinationType) initDestination else it.defaultDestination } - view.initView(startMenuIndex, destinations) + view.initView(startMenuIndex, rootAppMenuItems, destinations) if (initDestination != null && startMenuIndex == 4) { view.openMoreDestination(initDestination) } @@ -101,7 +97,6 @@ class MainPresenter @Inject constructor( fun onViewChange(destinationView: BaseView) { view?.apply { showBottomNavigation(shouldShowBottomNavigation(destinationView)) - showActionBarElevation(shouldShowActionBarElevation(destinationView)) currentViewTitle?.let { setViewTitle(it) } currentViewSubtitle?.let { setViewSubTitle(it.ifBlank { null }) } currentStackSize?.let { @@ -111,17 +106,11 @@ class MainPresenter @Inject constructor( } } - private fun shouldShowActionBarElevation(destination: BaseView) = when (destination) { - is GradeView, - is MessageView, - is SchoolAndTeachersView -> false - else -> true - } - private fun shouldShowBottomNavigation(destination: BaseView) = when (destination) { is AccountView, is StudentInfoView, is AccountDetailsView -> false + else -> true } @@ -139,12 +128,9 @@ class MainPresenter @Inject constructor( return true } - fun onBackPressed(default: () -> Unit) { + fun onBackPressed() { Timber.i("Back pressed in main view") - view?.run { - if (isRootView) default() - else popView() - } + view?.popView() } fun onTabSelected(index: Int, wasSelected: Boolean): Boolean { @@ -163,20 +149,8 @@ class MainPresenter @Inject constructor( } fun onEnableAdsSelected() { - view?.showPrivacyPolicyDialog() - } - - fun onPrivacyAgree(isPersonalizedAds: Boolean) { - preferencesRepository.isAgreeToProcessData = true - preferencesRepository.isPersonalizedAdsEnabled = isPersonalizedAds - - adsHelper.initialize() - preferencesRepository.isAdsEnabled = true - } - - fun onPrivacySelected() { - view?.openPrivacyPolicy() + adsHelper.initialize() } private fun checkInAppReview() { @@ -204,8 +178,8 @@ class MainPresenter @Inject constructor( .getOrElse { return@launch } if (Instant.now().minus(Duration.ofDays(28)).isAfter(student.registrationDate)) { - view?.showAppSupport() preferencesRepository.isAppSupportShown = true + view?.showAppSupport() } } } 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 3d018e3d6..70a94fc81 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 @@ -4,6 +4,7 @@ import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.ui.base.BaseView import io.github.wulkanowy.ui.modules.Destination +import io.github.wulkanowy.ui.modules.settings.appearance.menuorder.AppMenuItem interface MainView : BaseView { @@ -15,7 +16,11 @@ interface MainView : BaseView { val currentStackSize: Int? - fun initView(startMenuIndex: Int, rootDestinations: List) + fun initView( + startMenuIndex: Int, + rootAppMenuItems: List, + rootUpdatedDestinations: List + ) fun switchMenuView(position: Int) @@ -23,8 +28,6 @@ interface MainView : BaseView { fun showAccountPicker(studentWithSemesters: List) - fun showActionBarElevation(show: Boolean) - fun showBottomNavigation(show: Boolean) fun notifyMenuViewReselected() @@ -43,10 +46,6 @@ interface MainView : BaseView { 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 4607793c9..02bc13a16 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 @@ -11,10 +11,13 @@ 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.* +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.databinding.FragmentMessageBinding import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.base.BaseFragmentPagerAdapter +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.modules.message.tab.MessageTabFragment @@ -24,7 +27,7 @@ import javax.inject.Inject @AndroidEntryPoint class MessageFragment : BaseFragment(R.layout.fragment_message), - MessageView, MainView.TitledView { + MessageView, MainView.TitledView, MainView.MainChildView { @Inject lateinit var presenter: MessagePresenter @@ -48,6 +51,7 @@ class MessageFragment : BaseFragment(R.layout.fragment_m override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding = FragmentMessageBinding.bind(view) + messageContainer = binding.messageViewPager presenter.onAttachView(this) } @@ -94,6 +98,10 @@ class MessageFragment : BaseFragment(R.layout.fragment_m binding.messageProgress.visibility = if (show) VISIBLE else INVISIBLE } + override fun showMessage(messageId: Int) { + showMessage(getString(messageId)) + } + override fun showNewMessage(show: Boolean) { binding.openSendMessageButton.run { if (show) show() else hide() @@ -123,8 +131,12 @@ class MessageFragment : BaseFragment(R.layout.fragment_m presenter.onChildViewShowNewMessage(show) } - fun onFragmentChanged() { - presenter.onFragmentChanged() + override fun onFragmentReselected() { + if (::presenter.isInitialized) presenter.onFragmentReselected() + } + + override fun onFragmentChanged() { + if (::presenter.isInitialized) presenter.onFragmentChanged() } override fun notifyChildLoadData(index: Int, forceRefresh: Boolean) { @@ -139,10 +151,19 @@ class MessageFragment : BaseFragment(R.layout.fragment_m } } + override fun notifyChildParentReselected(index: Int) { + (pagerAdapter.getFragmentInstance(index) as? MessageTabFragment) + ?.onParentReselected() + } + override fun openSendMessage() { context?.let { it.startActivity(SendMessageActivity.getStartIntent(it)) } } + override fun popView() { + (activity as? MainActivity)?.popView() + } + override fun onDestroyView() { presenter.onDetachView() super.onDestroyView() 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 68bdc4b7c..37a2d422a 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 @@ -1,5 +1,7 @@ package io.github.wulkanowy.ui.modules.message +import io.github.wulkanowy.R +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 @@ -9,7 +11,8 @@ import javax.inject.Inject class MessagePresenter @Inject constructor( errorHandler: ErrorHandler, - studentRepository: StudentRepository + studentRepository: StudentRepository, + private val preferencesRepository: PreferencesRepository, ) : BasePresenter(errorHandler, studentRepository) { override fun onAttachView(view: MessageView) { @@ -19,6 +22,14 @@ class MessagePresenter @Inject constructor( Timber.i("Message view was initialized") loadData() } + + showIncognitoModeReminderMessage() + } + + private fun showIncognitoModeReminderMessage() { + if (preferencesRepository.isIncognitoMode) { + view?.showMessage(R.string.message_incognito_mode_on) + } } fun onPageSelected(index: Int) { @@ -39,6 +50,14 @@ class MessagePresenter @Inject constructor( view?.notifyChildrenFinishActionMode() } + fun onFragmentReselected() { + Timber.i("Message view is reselected") + view?.run { + popView() + notifyChildParentReselected(currentPageIndex) + } + } + fun onChildViewLoaded() { view?.apply { showContent(true) 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 e0cc5098c..7fdc6e181 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 @@ -1,5 +1,6 @@ package io.github.wulkanowy.ui.modules.message +import androidx.annotation.StringRes import io.github.wulkanowy.ui.base.BaseView interface MessageView : BaseView { @@ -12,6 +13,8 @@ interface MessageView : BaseView { fun showProgress(show: Boolean) + fun showMessage(@StringRes messageId: Int) + fun showNewMessage(show: Boolean) fun showTabLayout(show: Boolean) @@ -20,5 +23,9 @@ interface MessageView : BaseView { fun notifyChildrenFinishActionMode() + fun notifyChildParentReselected(index: Int) + fun openSendMessage() + + fun popView() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/mailboxchooser/MailboxChooserAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/mailboxchooser/MailboxChooserAdapter.kt new file mode 100644 index 000000000..59f6d288d --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/mailboxchooser/MailboxChooserAdapter.kt @@ -0,0 +1,81 @@ +package io.github.wulkanowy.ui.modules.message.mailboxchooser + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.core.view.isVisible +import androidx.recyclerview.widget.DiffUtil.ItemCallback +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import io.github.wulkanowy.R +import io.github.wulkanowy.data.db.entities.Mailbox +import io.github.wulkanowy.data.db.entities.MailboxType +import io.github.wulkanowy.databinding.ItemMailboxChooserBinding +import javax.inject.Inject + +class MailboxChooserAdapter @Inject constructor() : + ListAdapter(Differ) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ItemViewHolder( + ItemMailboxChooserBinding.inflate( + LayoutInflater.from(parent.context), parent, false + ) + ) + + override fun onBindViewHolder(holder: ItemViewHolder, position: Int) { + holder.bind(getItem(position)) + } + + class ItemViewHolder( + private val binding: ItemMailboxChooserBinding, + ) : RecyclerView.ViewHolder(binding.root) { + + fun bind(item: MailboxChooserItem) { + with(binding) { + mailboxItemName.text = item.mailbox?.getFirstLine() + ?: root.resources.getString(R.string.message_chip_all_mailboxes) + mailboxItemSchool.text = item.mailbox?.getSecondLine() + mailboxItemSchool.isVisible = !item.isAll + + root.setOnClickListener { item.onClickListener(item.mailbox) } + } + } + + private fun Mailbox.getFirstLine() = buildString { + if (studentName.isNotBlank() && studentName != userName) { + append(studentName) + append(" - ") + } + append(userName) + } + + private fun Mailbox.getSecondLine() = buildString { + append(schoolNameShort) + append(" - ") + append(getMailboxType(type)) + } + + private fun getMailboxType(type: MailboxType): String = when (type) { + MailboxType.STUDENT -> R.string.message_mailbox_type_student + MailboxType.PARENT -> R.string.message_mailbox_type_parent + MailboxType.GUARDIAN -> R.string.message_mailbox_type_guardian + MailboxType.EMPLOYEE -> R.string.message_mailbox_type_employee + MailboxType.UNKNOWN -> null + }.let { it?.let { it1 -> binding.root.resources.getString(it1) }.orEmpty() } + } + + private object Differ : ItemCallback() { + override fun areItemsTheSame( + oldItem: MailboxChooserItem, + newItem: MailboxChooserItem + ): Boolean { + return oldItem.mailbox?.globalKey == newItem.mailbox?.globalKey + } + + override fun areContentsTheSame( + oldItem: MailboxChooserItem, + newItem: MailboxChooserItem + ): Boolean { + return oldItem == newItem + } + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/mailboxchooser/MailboxChooserDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/mailboxchooser/MailboxChooserDialog.kt new file mode 100644 index 000000000..8bd84f2bf --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/mailboxchooser/MailboxChooserDialog.kt @@ -0,0 +1,75 @@ +package io.github.wulkanowy.ui.modules.message.mailboxchooser + +import android.app.Dialog +import android.os.Bundle +import android.view.View +import androidx.core.os.bundleOf +import androidx.fragment.app.setFragmentResult +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import dagger.hilt.android.AndroidEntryPoint +import io.github.wulkanowy.data.db.entities.Mailbox +import io.github.wulkanowy.databinding.DialogMailboxChooserBinding +import io.github.wulkanowy.ui.base.BaseDialogFragment +import io.github.wulkanowy.utils.parcelableArray +import javax.inject.Inject + +@AndroidEntryPoint +class MailboxChooserDialog : BaseDialogFragment(), MailboxChooserView { + + @Inject + lateinit var presenter: MailboxChooserPresenter + + @Inject + lateinit var mailboxAdapter: MailboxChooserAdapter + + companion object { + const val LISTENER_KEY = "mailbox_selected" + const val MAILBOX_KEY = "selected_mailbox" + const val REQUIRED_KEY = "is_mailbox_required" + + fun newInstance(mailboxes: List, isMailboxRequired: Boolean, folder: String) = + MailboxChooserDialog().apply { + arguments = bundleOf( + MAILBOX_KEY to mailboxes.toTypedArray(), + REQUIRED_KEY to isMailboxRequired, + LISTENER_KEY to folder, + ) + } + } + + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return MaterialAlertDialogBuilder(requireContext(), theme) + .setView( + DialogMailboxChooserBinding.inflate(layoutInflater).apply { binding = this }.root + ) + .create() + } + + + @Suppress("UNCHECKED_CAST") + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + presenter.onAttachView( + view = this, + requireMailbox = requireArguments().getBoolean(REQUIRED_KEY, false), + mailboxes = requireArguments().parcelableArray(MAILBOX_KEY).orEmpty().toList(), + ) + } + + override fun initView() { + binding.accountQuickDialogRecycler.adapter = mailboxAdapter + } + + override fun submitData(items: List) { + mailboxAdapter.submitList(items) + } + + override fun onMailboxSelected(item: Mailbox?) { + setFragmentResult( + requestKey = requireArguments().getString(LISTENER_KEY).orEmpty(), + result = bundleOf(MAILBOX_KEY to item), + ) + dismiss() + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/mailboxchooser/MailboxChooserItem.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/mailboxchooser/MailboxChooserItem.kt new file mode 100644 index 000000000..6923cf085 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/mailboxchooser/MailboxChooserItem.kt @@ -0,0 +1,9 @@ +package io.github.wulkanowy.ui.modules.message.mailboxchooser + +import io.github.wulkanowy.data.db.entities.Mailbox + +data class MailboxChooserItem( + val mailbox: Mailbox? = null, + val isAll: Boolean = false, + val onClickListener: (Mailbox?) -> Unit, +) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/mailboxchooser/MailboxChooserPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/mailboxchooser/MailboxChooserPresenter.kt new file mode 100644 index 000000000..5bd7c84ab --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/mailboxchooser/MailboxChooserPresenter.kt @@ -0,0 +1,38 @@ +package io.github.wulkanowy.ui.modules.message.mailboxchooser + +import io.github.wulkanowy.data.db.entities.Mailbox +import io.github.wulkanowy.data.repositories.StudentRepository +import io.github.wulkanowy.ui.base.BasePresenter +import io.github.wulkanowy.ui.base.ErrorHandler +import timber.log.Timber +import javax.inject.Inject + +class MailboxChooserPresenter @Inject constructor( + errorHandler: ErrorHandler, + studentRepository: StudentRepository +) : BasePresenter(errorHandler, studentRepository) { + + fun onAttachView(view: MailboxChooserView, mailboxes: List, requireMailbox: Boolean) { + super.onAttachView(view) + + view.initView() + Timber.i("Mailbox chooser view was initialized") + view.submitData(getMailboxItems(mailboxes, requireMailbox)) + } + + private fun getMailboxItems( + mailboxes: List, + requireMailbox: Boolean, + ): List = buildList { + if (!requireMailbox) { + add(MailboxChooserItem(isAll = true, onClickListener = ::onMailboxSelect)) + } + addAll(mailboxes.map { + MailboxChooserItem(mailbox = it, isAll = false, onClickListener = ::onMailboxSelect) + }) + } + + fun onMailboxSelect(item: Mailbox?) { + view?.onMailboxSelected(item) + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/mailboxchooser/MailboxChooserView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/mailboxchooser/MailboxChooserView.kt new file mode 100644 index 000000000..2e20ee815 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/mailboxchooser/MailboxChooserView.kt @@ -0,0 +1,13 @@ +package io.github.wulkanowy.ui.modules.message.mailboxchooser + +import io.github.wulkanowy.data.db.entities.Mailbox +import io.github.wulkanowy.ui.base.BaseView + +interface MailboxChooserView : BaseView { + + fun initView() + + fun submitData(items: List) + + fun onMailboxSelected(item: Mailbox?) +} 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 3c1c53d39..d3c6b95c7 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 @@ -1,6 +1,5 @@ package io.github.wulkanowy.ui.modules.message.preview -import android.annotation.SuppressLint import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -74,15 +73,20 @@ class MessagePreviewAdapter @Inject constructor() : } } - @SuppressLint("SetTextI18n") private fun bindMessage(holder: MessageViewHolder, message: Message) { val context = holder.binding.root.context + val recipientCount = (message.unreadBy ?: 0) + (message.readBy ?: 0) + val isReceived = message.unreadBy == null - val readTextValue = when { - !message.unread -> R.string.all_yes - else -> R.string.all_no + val readText = when { + recipientCount > 1 -> { + context.getString(R.string.message_read_by, message.readBy, recipientCount) + } + message.readBy == 1 || (isReceived && !message.unread) -> { + context.getString(R.string.message_read, context.getString(R.string.all_yes)) + } + else -> context.getString(R.string.message_read, context.getString(R.string.all_no)) } - val readText = context.getString(R.string.message_read, context.getString(readTextValue)) with(holder.binding) { messagePreviewSubject.text = message.subject.ifBlank { 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 2a5523f4d..3ed685cd7 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 @@ -12,7 +12,9 @@ import android.view.View.VISIBLE import android.webkit.WebResourceRequest import android.webkit.WebView import android.webkit.WebViewClient +import androidx.annotation.StringRes import androidx.core.content.getSystemService +import androidx.core.os.bundleOf import androidx.recyclerview.widget.LinearLayoutManager import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R @@ -23,6 +25,7 @@ import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.modules.main.MainActivity import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.modules.message.send.SendMessageActivity +import io.github.wulkanowy.utils.serializable import io.github.wulkanowy.utils.shareText import javax.inject.Inject @@ -66,13 +69,12 @@ class MessagePreviewFragment : companion object { const val MESSAGE_ID_KEY = "message_id" - fun newInstance(message: Message): MessagePreviewFragment { - return MessagePreviewFragment().apply { - arguments = Bundle().apply { putSerializable(MESSAGE_ID_KEY, message) } - } + fun newInstance(message: Message) = MessagePreviewFragment().apply { + arguments = bundleOf(MESSAGE_ID_KEY to message) } } + @Suppress("DEPRECATION") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setHasOptionsMenu(true) @@ -83,8 +85,8 @@ class MessagePreviewFragment : binding = FragmentMessagePreviewBinding.bind(view) messageContainer = binding.messagePreviewContainer presenter.onAttachView( - this, - (savedInstanceState ?: arguments)?.getSerializable(MESSAGE_ID_KEY) as? Message + view = this, + message = (savedInstanceState ?: arguments)?.serializable(MESSAGE_ID_KEY), ) } @@ -163,6 +165,10 @@ class MessagePreviewFragment : binding.messagePreviewErrorRetry.setOnClickListener { callback() } } + override fun showMessage(@StringRes messageId: Int) { + showMessage(getString(messageId)) + } + override fun openMessageReply(message: Message?) { context?.let { it.startActivity(SendMessageActivity.getStartIntent(it, message, true)) } } 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 c011f41f3..cd7b72843 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 @@ -2,12 +2,13 @@ package io.github.wulkanowy.ui.modules.message.preview import android.annotation.SuppressLint import androidx.core.text.parseAsHtml +import io.github.wulkanowy.R import io.github.wulkanowy.data.* import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.db.entities.MessageAttachment import io.github.wulkanowy.data.enums.MessageFolder -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.repositories.StudentRepository import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.ErrorHandler @@ -21,7 +22,7 @@ class MessagePreviewPresenter @Inject constructor( errorHandler: ErrorHandler, studentRepository: StudentRepository, private val messageRepository: MessageRepository, - private val mailboxRepository: MailboxRepository, + private val preferencesRepository: PreferencesRepository, private val analytics: AnalyticsHelper ) : BasePresenter(errorHandler, studentRepository) { @@ -56,7 +57,11 @@ class MessagePreviewPresenter @Inject constructor( private fun loadData(messageToLoad: Message) { flatResourceFlow { val student = studentRepository.getCurrentStudent() - messageRepository.getMessage(student, messageToLoad, true) + messageRepository.getMessage( + student = student, + message = messageToLoad, + markAsRead = !preferencesRepository.isIncognitoMode, + ) } .logResourceStatus("message ${messageToLoad.messageId} preview") .onResourceData { @@ -67,6 +72,10 @@ class MessagePreviewPresenter @Inject constructor( setMessageWithAttachment(it) showContent(true) initOptions() + + if (preferencesRepository.isIncognitoMode && it.message.unread) { + showMessage(R.string.message_incognito_description) + } } } else { view?.run { @@ -187,7 +196,7 @@ class MessagePreviewPresenter @Inject constructor( presenterScope.launch { runCatching { val student = studentRepository.getCurrentStudent(decryptPass = true) - val mailbox = mailboxRepository.getMailbox(student) + val mailbox = messageRepository.getMailboxByStudent(student) messageRepository.deleteMessage(student, mailbox, message!!) } .onFailure { 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 c5a947939..7f5f140b2 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewView.kt @@ -1,5 +1,6 @@ package io.github.wulkanowy.ui.modules.message.preview +import androidx.annotation.StringRes import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.db.entities.MessageWithAttachment import io.github.wulkanowy.ui.base.BaseView @@ -43,4 +44,6 @@ interface MessagePreviewView : BaseView { fun popView() fun printDocument(html: String, jobName: String) + + fun showMessage(@StringRes messageId: Int) } 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 334e389e2..0ba82f1a0 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 @@ -1,10 +1,10 @@ package io.github.wulkanowy.ui.modules.message.send import android.annotation.SuppressLint -import android.app.AlertDialog import android.content.Context import android.content.Intent import android.graphics.Rect +import android.os.Build import android.os.Bundle import android.text.Spanned import android.view.Menu @@ -12,18 +12,26 @@ import android.view.MenuItem import android.view.TouchDelegate import android.view.View.GONE import android.view.View.VISIBLE +import android.view.ViewGroup import android.widget.Toast import android.widget.Toast.LENGTH_LONG import androidx.core.text.parseAsHtml import androidx.core.text.toHtml +import androidx.core.view.* import androidx.core.widget.doOnTextChanged +import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R +import io.github.wulkanowy.data.db.entities.Mailbox import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.databinding.ActivitySendMessageBinding import io.github.wulkanowy.ui.base.BaseActivity +import io.github.wulkanowy.ui.modules.message.mailboxchooser.MailboxChooserDialog +import io.github.wulkanowy.ui.modules.message.mailboxchooser.MailboxChooserDialog.Companion.LISTENER_KEY +import io.github.wulkanowy.ui.modules.message.mailboxchooser.MailboxChooserDialog.Companion.MAILBOX_KEY import io.github.wulkanowy.utils.dpToPx import io.github.wulkanowy.utils.hideSoftInput +import io.github.wulkanowy.utils.nullableSerializable import io.github.wulkanowy.utils.showSoftInput import javax.inject.Inject @@ -94,19 +102,30 @@ class SendMessageActivity : BaseActivity= Build.VERSION_CODES.R) { + WindowCompat.setDecorFitsSystemWindows(window, false) + binding.sendAppBar.isLifted = true + } + initializeMessageContainer() + messageContainer = binding.sendMessageContainer formRecipientsData = binding.sendMessageTo.addedChipItems as List formSubjectValue = binding.sendMessageSubject.text.toString() formContentValue = binding.sendMessageMessageContent.text.toString().parseAsHtml().toString() + binding.sendMessageFrom.setOnClickListener { presenter.onOpenMailboxChooser() } presenter.onAttachView( view = this, - reason = intent.getSerializableExtra(EXTRA_REASON) as? String, - message = intent.getSerializableExtra(EXTRA_MESSAGE) as? Message, - reply = intent.getSerializableExtra(EXTRA_REPLY) as? Boolean + reason = intent.nullableSerializable(EXTRA_REASON), + message = intent.nullableSerializable(EXTRA_MESSAGE), + reply = intent.nullableSerializable(EXTRA_REPLY) ) + supportFragmentManager.setFragmentResultListener(LISTENER_KEY, this) { _, bundle -> + presenter.onMailboxSelected(bundle.nullableSerializable(MAILBOX_KEY)) + } } @SuppressLint("ClickableViewAccessibility") @@ -121,6 +140,22 @@ class SendMessageActivity : BaseActivity + val navigationBarInsets = insets.getInsets(WindowInsetsCompat.Type.navigationBars()) + val imeInsets = insets.getInsets(WindowInsetsCompat.Type.ime()) + + view.updateLayoutParams { + bottomMargin = if (imeInsets.bottom > navigationBarInsets.bottom) { + imeInsets.bottom + } else { + navigationBarInsets.bottom + } + } + WindowInsetsCompat.CONSUMED + } + } + private fun onMessageSubjectChange(text: CharSequence?) { formSubjectValue = text.toString() presenter.onMessageContentChange() @@ -205,6 +240,14 @@ class SendMessageActivity : BaseActivity) { + MailboxChooserDialog.newInstance( + mailboxes = mailboxes, + isMailboxRequired = true, + folder = LISTENER_KEY, + ).show(supportFragmentManager, "chooser") + } + override fun popView() { finish() } @@ -235,7 +278,7 @@ class SendMessageActivity : BaseActivity presenter.restoreMessageParts() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/SendMessagePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/SendMessagePresenter.kt index 6c07ee6ad..e776e9941 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/SendMessagePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/SendMessagePresenter.kt @@ -1,15 +1,15 @@ package io.github.wulkanowy.ui.modules.message.send -import io.github.wulkanowy.data.Resource +import io.github.wulkanowy.data.* import io.github.wulkanowy.data.db.entities.Mailbox import io.github.wulkanowy.data.db.entities.MailboxType import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.db.entities.Recipient -import io.github.wulkanowy.data.logResourceStatus -import io.github.wulkanowy.data.onResourceNotLoading import io.github.wulkanowy.data.pojos.MessageDraft -import io.github.wulkanowy.data.repositories.* -import io.github.wulkanowy.data.resourceFlow +import io.github.wulkanowy.data.repositories.MessageRepository +import io.github.wulkanowy.data.repositories.PreferencesRepository +import io.github.wulkanowy.data.repositories.RecipientRepository +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 @@ -28,7 +28,6 @@ class SendMessagePresenter @Inject constructor( errorHandler: ErrorHandler, studentRepository: StudentRepository, private val messageRepository: MessageRepository, - private val mailboxRepository: MailboxRepository, private val recipientRepository: RecipientRepository, private val preferencesRepository: PreferencesRepository, private val analytics: AnalyticsHelper @@ -36,10 +35,19 @@ class SendMessagePresenter @Inject constructor( private val messageUpdateChannel = Channel() + private var message: Message? = null + private var isReplay: Boolean? = null + + private var mailboxes: List = emptyList() + private var selectedMailbox: Mailbox? = null + fun onAttachView(view: SendMessageView, reason: String?, message: Message?, reply: Boolean?) { super.onAttachView(view) view.initView() initializeSubjectStream() + this.message = message + this.isReplay = reply + Timber.i("Send message view was initialized") loadData(message, reply) with(view) { @@ -47,7 +55,7 @@ class SendMessagePresenter @Inject constructor( view.showMessageBackupDialog() } reason?.let { - setSubject("Usprawiedliwenie") + setSubject("Usprawiedliwienie") setContent(it) } message?.let { @@ -110,16 +118,31 @@ class SendMessagePresenter @Inject constructor( return false } + fun onOpenMailboxChooser() { + view?.showMailboxChooser(mailboxes) + } + + fun onMailboxSelected(mailbox: Mailbox?) { + selectedMailbox = mailbox + + loadData(message, isReplay) + } + private fun loadData(message: Message?, reply: Boolean?) { resourceFlow { val student = studentRepository.getCurrentStudent() - val mailbox = mailboxRepository.getMailbox(student) + + if (selectedMailbox == null && mailboxes.isEmpty()) { + selectedMailbox = messageRepository.getMailboxByStudent(student) + mailboxes = messageRepository.getMailboxes(student, false).toFirstResult() + .dataOrNull.orEmpty() + } Timber.i("Loading recipients started") val recipients = createChips( recipients = recipientRepository.getRecipients( student = student, - mailbox = mailbox, + mailbox = selectedMailbox, type = MailboxType.EMPLOYEE, ) ) @@ -130,7 +153,7 @@ class SendMessagePresenter @Inject constructor( message != null && reply == true -> recipientRepository.getMessageSender( student = student, message = message, - mailbox = mailbox, + mailbox = selectedMailbox, ) else -> emptyList() }.let { createChips(it) } @@ -139,39 +162,42 @@ class SendMessagePresenter @Inject constructor( messageRecipients.size ) - Triple(mailbox, recipients, messageRecipients) + recipients to 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 - ) - showContent(true) - } - } - is Resource.Error -> { - view?.showContent(true) - errorHandler.dispatch(it.error) + .onResourceLoading { + view?.run { + showProgress(true) + showContent(false) + } + } + .onResourceNotLoading { + view?.run { showProgress(false) } + } + .onResourceError { + view?.showContent(true) + errorHandler.dispatch(it) + } + .onResourceSuccess { + it.let { (recipientChips, selectedRecipientChips) -> + view?.run { + setMailbox(getMailboxName(selectedMailbox)) + setRecipients(recipientChips) + if (selectedRecipientChips.isNotEmpty()) setSelectedRecipients( + selectedRecipientChips + ) + showContent(true) } } - }.onResourceNotLoading { - view?.run { showProgress(false) } - }.launch() + } + .launch() } private fun sendMessage(subject: String, content: String, recipients: List) { + val mailbox = selectedMailbox ?: return + resourceFlow { val student = studentRepository.getCurrentStudent() - val mailbox = mailboxRepository.getMailbox(student) messageRepository.sendMessage( student = student, subject = subject, @@ -222,18 +248,21 @@ class SendMessagePresenter @Inject constructor( } } - private fun getMailboxName(mailbox: Mailbox): String { + private fun getMailboxName(mailbox: Mailbox?): String { + mailbox ?: return "" + + // username - accountType [\n student name - ] (school short name) return buildString { append(mailbox.userName) append(" - ") append(getMailboxType(mailbox.type)) + appendLine() if (mailbox.type == MailboxType.PARENT) { - append(" - ") append(mailbox.studentName) + append(" - ") } - append(" - ") append("(${mailbox.schoolNameShort})") } } @@ -267,9 +296,9 @@ class SendMessagePresenter @Inject constructor( private fun saveDraftMessage() { messageRepository.draftMessage = MessageDraft( - view?.formRecipientsData!!, - view?.formSubjectValue!!, - view?.formContentValue!! + recipients = view?.formRecipientsData!!, + subject = view?.formSubjectValue!!, + content = view?.formContentValue!!, ) } 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 1057114b8..e27a09d60 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 @@ -61,4 +61,5 @@ interface SendMessageView : BaseView { fun getMessageBackupDialogStringWithRecipients(recipients: String): String fun clearDraft() + fun showMailboxChooser(mailboxes: 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 55f03ef84..9792c7085 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 @@ -1,28 +1,35 @@ package io.github.wulkanowy.ui.modules.message.tab +import android.annotation.SuppressLint +import android.content.res.ColorStateList import android.graphics.Typeface import android.view.LayoutInflater import android.view.ViewGroup import android.widget.CompoundButton import androidx.core.view.isVisible +import androidx.core.widget.ImageViewCompat import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import io.github.wulkanowy.R import io.github.wulkanowy.databinding.ItemMessageBinding import io.github.wulkanowy.databinding.ItemMessageChipsBinding +import io.github.wulkanowy.utils.getCompatColor +import io.github.wulkanowy.utils.getThemeAttrColor import io.github.wulkanowy.utils.toFormattedString import javax.inject.Inject class MessageTabAdapter @Inject constructor() : RecyclerView.Adapter() { - var onItemClickListener: (MessageTabDataItem.MessageItem, position: Int) -> Unit = { _, _ -> } + lateinit var onItemClickListener: (MessageTabDataItem.MessageItem, position: Int) -> Unit - var onLongItemClickListener: (MessageTabDataItem.MessageItem) -> Unit = {} + lateinit var onLongItemClickListener: (MessageTabDataItem.MessageItem) -> Unit - var onHeaderClickListener: (CompoundButton, Boolean) -> Unit = { _, _ -> } + lateinit var onHeaderClickListener: (CompoundButton, Boolean) -> Unit - var onChangesDetectedListener = {} + lateinit var onMailboxClickListener: () -> Unit + + lateinit var onChangesDetectedListener: () -> Unit private var items = mutableListOf() @@ -46,12 +53,12 @@ class MessageTabAdapter @Inject constructor() : val inflater = LayoutInflater.from(parent.context) return when (MessageItemViewType.values()[viewType]) { - MessageItemViewType.MESSAGE -> ItemViewHolder( - ItemMessageBinding.inflate(inflater, parent, false) - ) MessageItemViewType.FILTERS -> HeaderViewHolder( ItemMessageChipsBinding.inflate(inflater, parent, false) ) + MessageItemViewType.MESSAGE -> ItemViewHolder( + ItemMessageBinding.inflate(inflater, parent, false) + ) } } @@ -62,10 +69,26 @@ class MessageTabAdapter @Inject constructor() : } } + @SuppressLint("PrivateResource") private fun bindHeaderViewHolder(holder: HeaderViewHolder, position: Int) { val item = items[position] as MessageTabDataItem.FilterHeader + val context = holder.binding.root.context with(holder.binding) { + chipMailbox.text = + item.selectedMailbox ?: context.getString(R.string.message_chip_all_mailboxes) + chipMailbox.chipBackgroundColor = ColorStateList.valueOf( + if (item.selectedMailbox == null) { + context.getCompatColor(R.color.m3_elevated_chip_background_color) + } else context.getThemeAttrColor(R.attr.colorPrimary, 64) + ) + chipMailbox.setTextColor( + if (item.selectedMailbox == null) { + context.getThemeAttrColor(R.attr.colorOnSurfaceVariant) + } else context.getThemeAttrColor(R.attr.colorPrimary) + ) + chipMailbox.setOnClickListener { onMailboxClickListener() } + if (item.onlyUnread == null) { chipUnread.isVisible = false } else { @@ -74,6 +97,7 @@ class MessageTabAdapter @Inject constructor() : chipUnread.setOnCheckedChangeListener(onHeaderClickListener) } chipUnread.isEnabled = item.isEnabled + chipAttachments.isEnabled = item.isEnabled chipAttachments.isChecked = item.onlyWithAttachments chipAttachments.setOnCheckedChangeListener(onHeaderClickListener) @@ -85,21 +109,35 @@ class MessageTabAdapter @Inject constructor() : val message = item.message with(holder.binding) { - val style = if (message.unread) Typeface.BOLD else Typeface.NORMAL + val normalFont = Typeface.create("sans-serif", Typeface.NORMAL) + val boldFont = Typeface.create("sans-serif-black", Typeface.NORMAL) + + val primaryColor = root.context.getThemeAttrColor(android.R.attr.textColorPrimary) + val secondaryColor = root.context.getThemeAttrColor(android.R.attr.textColorSecondary) + + val currentFont = if (message.unread) boldFont else normalFont + val currentTextColor = if (message.unread) primaryColor else secondaryColor with(messageItemAuthor) { text = message.correspondents - setTypeface(null, style) + setTextColor(currentTextColor) + typeface = currentFont } - messageItemSubject.run { + with(messageItemSubject) { text = message.subject.ifBlank { context.getString(R.string.message_no_subject) } - setTypeface(null, style) + setTextColor(currentTextColor) + typeface = currentFont } - messageItemDate.run { + with(messageItemDate) { text = message.date.toFormattedString() - setTypeface(null, style) + setTextColor(currentTextColor) + typeface = currentFont } - messageItemAttachmentIcon.isVisible = message.hasAttachments + with(messageItemAttachmentIcon) { + ImageViewCompat.setImageTintList(this, ColorStateList.valueOf(currentTextColor)) + isVisible = message.hasAttachments + } + messageItemUnreadIndicator.isVisible = message.unread root.setOnClickListener { holder.bindingAdapterPosition.let { @@ -111,7 +149,7 @@ class MessageTabAdapter @Inject constructor() : root.setOnLongClickListener { onLongItemClickListener(item) - return@setOnLongClickListener true + true } with(messageItemCheckbox) { 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 634dfc0e7..c0bd4170e 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 @@ -11,6 +11,7 @@ sealed class MessageTabDataItem(val viewType: MessageItemViewType) { ) : MessageTabDataItem(MessageItemViewType.MESSAGE) data class FilterHeader( + val selectedMailbox: String?, val onlyUnread: Boolean?, val onlyWithAttachments: Boolean, val isEnabled: Boolean 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 654b0e226..4364e8681 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 @@ -7,23 +7,29 @@ import android.view.MenuItem import android.view.View import android.view.View.* import android.widget.CompoundButton +import androidx.annotation.StringRes import androidx.appcompat.view.ActionMode import androidx.appcompat.widget.SearchView +import androidx.core.os.bundleOf import androidx.core.view.updatePadding +import androidx.fragment.app.setFragmentResultListener import androidx.recyclerview.widget.LinearLayoutManager import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R +import io.github.wulkanowy.data.db.entities.Mailbox import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.enums.MessageFolder import io.github.wulkanowy.databinding.FragmentMessageTabBinding import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.modules.main.MainActivity import io.github.wulkanowy.ui.modules.message.MessageFragment +import io.github.wulkanowy.ui.modules.message.mailboxchooser.MailboxChooserDialog import io.github.wulkanowy.ui.modules.message.preview.MessagePreviewFragment import io.github.wulkanowy.ui.widgets.DividerItemDecoration import io.github.wulkanowy.utils.dpToPx import io.github.wulkanowy.utils.getThemeAttrColor import io.github.wulkanowy.utils.hideSoftInput +import io.github.wulkanowy.utils.nullableSerializable import javax.inject.Inject @AndroidEntryPoint @@ -40,12 +46,8 @@ class MessageTabFragment : BaseFragment(R.layout.frag const val MESSAGE_TAB_FOLDER_ID = "message_tab_folder_id" - fun newInstance(folder: MessageFolder): MessageTabFragment { - return MessageTabFragment().apply { - arguments = Bundle().apply { - putString(MESSAGE_TAB_FOLDER_ID, folder.name) - } - } + fun newInstance(folder: MessageFolder) = MessageTabFragment().apply { + arguments = bundleOf(MESSAGE_TAB_FOLDER_ID to folder.name) } } @@ -83,6 +85,7 @@ class MessageTabFragment : BaseFragment(R.layout.frag } } + @Suppress("DEPRECATION") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setHasOptionsMenu(true) @@ -104,6 +107,7 @@ class MessageTabFragment : BaseFragment(R.layout.frag onItemClickListener = presenter::onMessageItemSelected onLongItemClickListener = presenter::onMessageItemLongSelected onHeaderClickListener = ::onChipChecked + onMailboxClickListener = presenter::onMailboxFilterSelected onChangesDetectedListener = ::resetListPosition } @@ -123,15 +127,28 @@ class MessageTabFragment : BaseFragment(R.layout.frag messageTabErrorRetry.setOnClickListener { presenter.onRetry() } messageTabErrorDetails.setOnClickListener { presenter.onDetailsClick() } } + + setFragmentResultListener(requireArguments().getString(MESSAGE_TAB_FOLDER_ID)!!) { _, bundle -> + presenter.onMailboxSelected( + mailbox = bundle.nullableSerializable(MailboxChooserDialog.MAILBOX_KEY), + ) + } } + @Deprecated("Deprecated in Java") + @Suppress("DEPRECATION") override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { super.onCreateOptionsMenu(menu, inflater) inflater.inflate(R.menu.action_menu_message_tab, menu) - val searchView = menu.findItem(R.id.action_search).actionView as SearchView - searchView.queryHint = getString(R.string.all_search_hint) - searchView.maxWidth = Int.MAX_VALUE + initializeSearchView(menu) + } + + private fun initializeSearchView(menu: Menu) { + val searchView = (menu.findItem(R.id.action_search).actionView as SearchView).apply { + queryHint = getString(R.string.all_search_hint) + maxWidth = Int.MAX_VALUE + } searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { override fun onQueryTextSubmit(query: String) = false override fun onQueryTextChange(query: String): Boolean { @@ -197,8 +214,8 @@ class MessageTabFragment : BaseFragment(R.layout.frag binding.messageTabSwipe.isRefreshing = show } - override fun showMessagesDeleted() { - showMessage(getString(R.string.message_messages_deleted)) + override fun showMessage(@StringRes messageId: Int) { + showMessage(getString(messageId)) } override fun notifyParentShowNewMessage(show: Boolean) { @@ -225,6 +242,10 @@ class MessageTabFragment : BaseFragment(R.layout.frag presenter.onParentFinishActionMode() } + fun onParentReselected() { + presenter.onParentReselected() + } + private fun onChipChecked(chip: CompoundButton, isChecked: Boolean) { when (chip.id) { R.id.chip_unread -> presenter.onUnreadFilterSelected(isChecked) @@ -246,6 +267,16 @@ class MessageTabFragment : BaseFragment(R.layout.frag ) } + override fun showMailboxChooser(mailboxes: List) { + (activity as? MainActivity)?.showDialogFragment( + MailboxChooserDialog.newInstance( + mailboxes = mailboxes, + isMailboxRequired = false, + folder = requireArguments().getString(MESSAGE_TAB_FOLDER_ID)!!, + ) + ) + } + override fun hideKeyboard() { activity?.hideSoftInput() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabPresenter.kt index 54711a689..90f93b145 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,9 +1,10 @@ package io.github.wulkanowy.ui.modules.message.tab +import io.github.wulkanowy.R import io.github.wulkanowy.data.* +import io.github.wulkanowy.data.db.entities.Mailbox import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.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 @@ -26,8 +27,7 @@ class MessageTabPresenter @Inject constructor( errorHandler: ErrorHandler, studentRepository: StudentRepository, private val messageRepository: MessageRepository, - private val mailboxRepository: MailboxRepository, - private val analytics: AnalyticsHelper + private val analytics: AnalyticsHelper, ) : BasePresenter(errorHandler, studentRepository) { lateinit var folder: MessageFolder @@ -36,6 +36,9 @@ class MessageTabPresenter @Inject constructor( private var lastSearchQuery = "" + private var mailboxes: List = emptyList() + private var selectedMailbox: Mailbox? = null + private var messages = emptyList() private val searchChannel = Channel() @@ -81,6 +84,14 @@ class MessageTabPresenter @Inject constructor( view?.showActionMode(false) } + fun onParentReselected() { + view?.run { + if (!isViewEmpty) { + resetListPosition() + } + } + } + fun onDestroyActionMode() { isActionMode = false messagesToDelete.clear() @@ -122,11 +133,10 @@ class MessageTabPresenter @Inject constructor( runCatching { val student = studentRepository.getCurrentStudent(true) - val mailbox = mailboxRepository.getMailbox(student) - messageRepository.deleteMessages(student, mailbox, messageList) + messageRepository.deleteMessages(student, selectedMailbox, messageList) } .onFailure(errorHandler::dispatch) - .onSuccess { view?.showMessagesDeleted() } + .onSuccess { view?.showMessage(R.string.message_messages_deleted) } } } @@ -202,13 +212,28 @@ class MessageTabPresenter @Inject constructor( } } + fun onMailboxFilterSelected() { + view?.showMailboxChooser(mailboxes) + } + + fun onMailboxSelected(mailbox: Mailbox?) { + selectedMailbox = mailbox + loadData(false) + } + private fun loadData(forceRefresh: Boolean) { Timber.i("Loading $folder message data started") flatResourceFlow { val student = studentRepository.getCurrentStudent() - val mailbox = mailboxRepository.getMailbox(student) - messageRepository.getMessages(student, mailbox, folder, forceRefresh) + + if (selectedMailbox == null && mailboxes.isEmpty()) { + selectedMailbox = messageRepository.getMailboxByStudent(student) + mailboxes = messageRepository.getMailboxes(student, forceRefresh).toFirstResult() + .dataOrNull.orEmpty() + } + + messageRepository.getMessages(student, selectedMailbox, folder, forceRefresh) } .logResourceStatus("load $folder message") .onResourceData { @@ -327,7 +352,16 @@ class MessageTabPresenter @Inject constructor( MessageTabDataItem.FilterHeader( onlyUnread = onlyUnread.takeIf { folder != MessageFolder.SENT }, onlyWithAttachments = onlyWithAttachments, - isEnabled = !isActionMode + isEnabled = !isActionMode, + selectedMailbox = selectedMailbox?.let { + buildString { + if (it.studentName.isNotBlank() && it.studentName != it.userName) { + append(it.studentName) + append(" - ") + } + append(it.userName) + } + }, ) ) 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 bfa43b209..247af4342 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 @@ -1,5 +1,7 @@ package io.github.wulkanowy.ui.modules.message.tab +import androidx.annotation.StringRes +import io.github.wulkanowy.data.db.entities.Mailbox import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.ui.base.BaseView @@ -25,7 +27,7 @@ interface MessageTabView : BaseView { fun showEmpty(show: Boolean) - fun showMessagesDeleted() + fun showMessage(@StringRes messageId: Int) fun showErrorView(show: Boolean) @@ -46,4 +48,6 @@ interface MessageTabView : BaseView { fun showActionMode(show: Boolean) fun showRecyclerBottomPadding(show: Boolean) + + fun showMailboxChooser(mailboxes: List) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/MobileDeviceFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/MobileDeviceFragment.kt index f8e367c57..1afe773cf 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/MobileDeviceFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/MobileDeviceFragment.kt @@ -22,7 +22,7 @@ import javax.inject.Inject @AndroidEntryPoint class MobileDeviceFragment : BaseFragment(R.layout.fragment_mobile_device), MobileDeviceView, - MainView.TitledView { + MainView.TitledView, MainView.MainChildView { @Inject lateinit var presenter: MobileDevicePresenter @@ -135,6 +135,14 @@ class MobileDeviceFragment : (activity as? MainActivity)?.showDialogFragment(MobileDeviceTokenDialog.newInstance()) } + override fun onFragmentReselected() { + if (::presenter.isInitialized) presenter.onFragmentReselected() + } + + override fun resetView() { + binding.mobileDevicesRecycler.smoothScrollToPosition(0) + } + override fun onDestroyView() { presenter.onDetachView() super.onDestroyView() 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 36a720e53..56785dbf4 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 @@ -129,4 +129,10 @@ class MobileDevicePresenter @Inject constructor( .onResourceError(errorHandler::dispatch) .launch("unregister") } + + fun onFragmentReselected() { + if (view?.isViewEmpty == false) { + view?.resetView() + } + } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/MobileDeviceView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/MobileDeviceView.kt index b94646a7b..973cb1234 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/MobileDeviceView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/MobileDeviceView.kt @@ -32,4 +32,6 @@ interface MobileDeviceView : BaseView { fun setErrorDetails(message: String) fun showTokenDialog() + + fun resetView() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/token/MobileDeviceTokenDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/token/MobileDeviceTokenDialog.kt index eb420a6ae..2cc2a2aa7 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/token/MobileDeviceTokenDialog.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/token/MobileDeviceTokenDialog.kt @@ -1,17 +1,17 @@ package io.github.wulkanowy.ui.modules.mobiledevice.token +import android.app.Dialog import android.content.ClipData import android.content.ClipboardManager import android.graphics.BitmapFactory import android.os.Bundle import android.util.Base64 -import android.view.LayoutInflater import android.view.View import android.view.View.GONE import android.view.View.VISIBLE -import android.view.ViewGroup import android.widget.Toast import androidx.core.content.getSystemService +import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.data.pojos.MobileDeviceToken @@ -31,17 +31,14 @@ class MobileDeviceTokenDialog : BaseDialogFragment(), fun newInstance() = MobileDeviceTokenDialog() } - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setStyle(STYLE_NO_TITLE, 0) + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return MaterialAlertDialogBuilder(requireContext(), theme) + .setView( + DialogMobileDeviceBinding.inflate(layoutInflater).apply { binding = this }.root + ) + .create() } - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ) = DialogMobileDeviceBinding.inflate(inflater).apply { binding = this }.root - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) presenter.onAttachView(this) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreAdapter.kt index 70587b0cf..697eab330 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreAdapter.kt @@ -1,6 +1,5 @@ package io.github.wulkanowy.ui.modules.more -import android.graphics.drawable.Drawable import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView @@ -9,9 +8,9 @@ import javax.inject.Inject class MoreAdapter @Inject constructor() : RecyclerView.Adapter() { - var items = emptyList>() + var items = emptyList() - var onClickListener: (name: String) -> Unit = {} + var onClickListener: (moreItem: MoreItem) -> Unit = {} override fun getItemCount() = items.size @@ -20,13 +19,14 @@ class MoreAdapter @Inject constructor() : RecyclerView.Adapter(R.layout.fragment_more), override val titleStringId: Int get() = R.string.more_title - override val messagesRes: Pair? - get() = context?.run { getString(R.string.message_title) to getCompatDrawable(R.drawable.ic_more_messages) } - - override val homeworkRes: Pair? - get() = context?.run { getString(R.string.homework_title) to getCompatDrawable(R.drawable.ic_more_homework) } - - override val noteRes: Pair? - get() = context?.run { getString(R.string.note_title) to getCompatDrawable(R.drawable.ic_more_note) } - - override val conferencesRes: Pair? - get() = context?.run { getString(R.string.conferences_title) to getCompatDrawable(R.drawable.ic_more_conferences) } - - override val schoolAnnouncementRes: Pair? - get() = context?.run { getString(R.string.school_announcement_title) to getCompatDrawable(R.drawable.ic_all_about) } - - override val schoolAndTeachersRes: Pair? - get() = context?.run { getString(R.string.schoolandteachers_title) to getCompatDrawable((R.drawable.ic_more_schoolandteachers)) } - - override val mobileDevicesRes: Pair? - get() = context?.run { getString(R.string.mobile_devices_title) to getCompatDrawable(R.drawable.ic_more_mobile_devices) } - - override val settingsRes: Pair? - get() = context?.run { getString(R.string.settings_title) to getCompatDrawable(R.drawable.ic_more_settings) } - - override val examRes: Pair? - get() = context?.run { getString(R.string.exam_title) to getCompatDrawable(R.drawable.ic_main_exam) } - - override val luckyNumberRes: Pair? - get() = context?.run { getString(R.string.lucky_number_title) to getCompatDrawable(R.drawable.ic_more_lucky_number) } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding = FragmentMoreBinding.bind(view) @@ -94,57 +54,21 @@ class MoreFragment : BaseFragment(R.layout.fragment_more), ?.onFragmentChanged() } - override fun updateData(data: List>) { + override fun updateData(data: List) { with(moreAdapter) { items = data notifyDataSetChanged() } } - override fun openMessagesView() { - (activity as? MainActivity)?.pushView(MessageFragment.newInstance()) - } - - override fun openHomeworkView() { - (activity as? MainActivity)?.pushView(HomeworkFragment.newInstance()) - } - - override fun openNoteView() { - (activity as? MainActivity)?.pushView(NoteFragment.newInstance()) - } - - override fun openSchoolAnnouncementView() { - (activity as? MainActivity)?.pushView(SchoolAnnouncementFragment.newInstance()) - } - - override fun openConferencesView() { - (activity as? MainActivity)?.pushView(ConferenceFragment.newInstance()) - } - - override fun openSchoolAndTeachersView() { - (activity as? MainActivity)?.pushView(SchoolAndTeachersFragment.newInstance()) - } - - override fun openMobileDevicesView() { - (activity as? MainActivity)?.pushView(MobileDeviceFragment.newInstance()) - } - - override fun openSettingsView() { - (activity as? MainActivity)?.pushView(SettingsFragment.newInstance()) - } - - override fun openExamView() { - (activity as? MainActivity)?.pushView(ExamFragment.newInstance()) - } - - override fun openLuckyNumberView() { - (activity as? MainActivity)?.pushView(LuckyNumberFragment.newInstance()) - } - override fun popView(depth: Int) { (activity as? MainActivity)?.popView(depth) } + override fun openView(destination: Destination) { + (activity as? MainActivity)?.pushView(destination.destinationFragment) + } + override fun onDestroyView() { presenter.onDetachView() super.onDestroyView() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreItem.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreItem.kt new file mode 100644 index 000000000..d544f041c --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreItem.kt @@ -0,0 +1,12 @@ +package io.github.wulkanowy.ui.modules.more + +import io.github.wulkanowy.ui.modules.Destination + +data class MoreItem( + + val icon: Int, + + val title: Int, + + val destination: Destination +) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/more/MorePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/more/MorePresenter.kt index 92551d6e9..0ebaf4c7b 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/more/MorePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/more/MorePresenter.kt @@ -1,16 +1,24 @@ package io.github.wulkanowy.ui.modules.more +import io.github.wulkanowy.R +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.Destination import timber.log.Timber import javax.inject.Inject class MorePresenter @Inject constructor( errorHandler: ErrorHandler, - studentRepository: StudentRepository + studentRepository: StudentRepository, + preferencesRepository: PreferencesRepository ) : BasePresenter(errorHandler, studentRepository) { + private val moreAppMenuItem = preferencesRepository.appMenuItemOrder + .sortedBy { it.order } + .drop(4) + override fun onAttachView(view: MoreView) { super.onAttachView(view) view.initView() @@ -18,22 +26,10 @@ class MorePresenter @Inject constructor( loadData() } - fun onItemSelected(title: String) { - Timber.i("Select more item \"${title}\"") - view?.run { - when (title) { - messagesRes?.first -> openMessagesView() - examRes?.first -> openExamView() - homeworkRes?.first -> openHomeworkView() - noteRes?.first -> openNoteView() - conferencesRes?.first -> openConferencesView() - schoolAnnouncementRes?.first -> openSchoolAnnouncementView() - schoolAndTeachersRes?.first -> openSchoolAndTeachersView() - mobileDevicesRes?.first -> openMobileDevicesView() - settingsRes?.first -> openSettingsView() - luckyNumberRes?.first -> openLuckyNumberView() - } - } + fun onItemSelected(moreItem: MoreItem) { + Timber.i("Select more item \"${moreItem.destination.destinationType}\"") + + view?.openView(moreItem.destination) } fun onViewReselected() { @@ -43,19 +39,21 @@ class MorePresenter @Inject constructor( private fun loadData() { Timber.i("Load items for more view") - view?.run { - updateData(listOfNotNull( - messagesRes, - examRes, - homeworkRes, - noteRes, - luckyNumberRes, - conferencesRes, - schoolAnnouncementRes, - schoolAndTeachersRes, - mobileDevicesRes, - settingsRes - )) + val moreItems = moreAppMenuItem.map { + MoreItem( + icon = it.icon, + title = it.title, + destination = it.destinationType.defaultDestination + ) } + .plus( + MoreItem( + icon = R.drawable.ic_more_settings, + title = R.string.settings_title, + destination = Destination.Settings + ) + ) + + view?.updateData(moreItems) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreView.kt index cb895de28..fbca97edc 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreView.kt @@ -1,53 +1,15 @@ package io.github.wulkanowy.ui.modules.more -import android.graphics.drawable.Drawable import io.github.wulkanowy.ui.base.BaseView +import io.github.wulkanowy.ui.modules.Destination interface MoreView : BaseView { - val messagesRes: Pair? - - val homeworkRes: Pair? - - val noteRes: Pair? - - val conferencesRes: Pair? - - val schoolAnnouncementRes: Pair? - - val schoolAndTeachersRes: Pair? - - val mobileDevicesRes: Pair? - - val settingsRes: Pair? - - val examRes: Pair? - - val luckyNumberRes: Pair? - fun initView() - fun updateData(data: List>) - - fun openSettingsView() + fun updateData(data: List) fun popView(depth: Int) - fun openMessagesView() - - fun openHomeworkView() - - fun openNoteView() - - fun openSchoolAnnouncementView() - - fun openConferencesView() - - fun openSchoolAndTeachersView() - - fun openMobileDevicesView() - - fun openExamView() - - fun openLuckyNumberView() + fun openView(destination: Destination) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/note/NoteDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/note/NoteDialog.kt index 5811456b6..0592e9243 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/note/NoteDialog.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/note/NoteDialog.kt @@ -1,23 +1,24 @@ package io.github.wulkanowy.ui.modules.note import android.annotation.SuppressLint +import android.app.Dialog import android.os.Bundle -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup import androidx.core.content.ContextCompat -import androidx.fragment.app.DialogFragment +import androidx.core.os.bundleOf +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Note import io.github.wulkanowy.databinding.DialogNoteBinding import io.github.wulkanowy.sdk.scrapper.notes.NoteCategory +import io.github.wulkanowy.ui.base.BaseDialogFragment import io.github.wulkanowy.utils.getThemeAttrColor -import io.github.wulkanowy.utils.lifecycleAwareVariable +import io.github.wulkanowy.utils.serializable import io.github.wulkanowy.utils.toFormattedString -class NoteDialog : DialogFragment() { - - private var binding: DialogNoteBinding by lifecycleAwareVariable() +@AndroidEntryPoint +class NoteDialog : BaseDialogFragment() { private lateinit var note: Note @@ -25,24 +26,21 @@ class NoteDialog : DialogFragment() { private const val ARGUMENT_KEY = "Item" - fun newInstance(exam: Note) = NoteDialog().apply { - arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, exam) } + fun newInstance(note: Note) = NoteDialog().apply { + arguments = bundleOf(ARGUMENT_KEY to note) } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setStyle(STYLE_NO_TITLE, 0) - arguments?.run { - note = getSerializable(ARGUMENT_KEY) as Note - } + note = requireArguments().serializable(ARGUMENT_KEY) } - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ) = DialogNoteBinding.inflate(inflater).apply { binding = this }.root + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return MaterialAlertDialogBuilder(requireContext(), theme) + .setView(DialogNoteBinding.inflate(layoutInflater).apply { binding = this }.root) + .create() + } @SuppressLint("SetTextI18n") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/note/NoteFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/note/NoteFragment.kt index dd6223448..cb54b3840 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/note/NoteFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/note/NoteFragment.kt @@ -18,7 +18,7 @@ import javax.inject.Inject @AndroidEntryPoint class NoteFragment : BaseFragment(R.layout.fragment_note), NoteView, - MainView.TitledView { + MainView.TitledView, MainView.MainChildView { @Inject lateinit var presenter: NotePresenter @@ -112,6 +112,14 @@ class NoteFragment : BaseFragment(R.layout.fragment_note), binding.noteSwipe.isRefreshing = show } + override fun onFragmentReselected() { + if (::presenter.isInitialized) presenter.onFragmentReselected() + } + + override fun resetView() { + binding.noteRecycler.smoothScrollToPosition(0) + } + override fun onDestroyView() { presenter.onDetachView() super.onDestroyView() 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 440565e11..62ad347fa 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 @@ -121,4 +121,10 @@ class NotePresenter @Inject constructor( } .launch("update_note") } + + fun onFragmentReselected() { + if (view?.isViewEmpty == false) { + view?.resetView() + } + } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/note/NoteView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/note/NoteView.kt index 9fc0be94c..b813b3159 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/note/NoteView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/note/NoteView.kt @@ -30,4 +30,6 @@ interface NoteView : BaseView { fun showRefresh(show: Boolean) fun showNoteDialog(note: Note) + + fun resetView() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/notifications/NotificationsFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/notifications/NotificationsFragment.kt new file mode 100644 index 000000000..0763d4fa8 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/notifications/NotificationsFragment.kt @@ -0,0 +1,64 @@ +package io.github.wulkanowy.ui.modules.notifications + +import android.os.Bundle +import android.view.View +import androidx.activity.result.contract.ActivityResultContracts.RequestPermission +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import dagger.hilt.android.AndroidEntryPoint +import io.github.wulkanowy.R +import io.github.wulkanowy.databinding.FragmentNotificationsBinding +import io.github.wulkanowy.ui.base.BaseFragment +import io.github.wulkanowy.ui.modules.login.LoginActivity +import io.github.wulkanowy.utils.openNotificationSettings + +@AndroidEntryPoint +class NotificationsFragment : + BaseFragment(R.layout.fragment_notifications) { + + private val permission = "android.permission.POST_NOTIFICATIONS" + + private val requestPermissionLauncher = registerForActivityResult(RequestPermission()) { + if (it) { + navigateToFinish() + } else showSettingsDialog() + } + + companion object { + fun newInstance() = NotificationsFragment() + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding = FragmentNotificationsBinding.bind(view) + initView() + } + + private fun initView() { + with(binding) { + notificationsSkip.setOnClickListener { navigateToFinish() } + notificationsEnable.setOnClickListener { requestPermission() } + } + } + + private fun showSettingsDialog() { + MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.notifications_header_title) + .setMessage(R.string.notifications_header_description) + .setNegativeButton(R.string.notifications_skip) { dialog, _ -> + dialog.dismiss() + navigateToFinish() + } + .setPositiveButton(R.string.pref_notification_go_to_settings) { _, _ -> + requireActivity().openNotificationSettings() + } + .show() + } + + private fun requestPermission() { + requestPermissionLauncher.launch(permission) + } + + private fun navigateToFinish() { + (requireActivity() as LoginActivity).navigateToFinish() + } +} 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 e2af05c92..fef06328d 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 @@ -58,7 +58,10 @@ class TeacherPresenter @Inject constructor( .logResourceStatus("load teachers data") .onResourceData { view?.run { - updateData(it.filter { item -> item.name.isNotBlank() }) + updateData(it + .filter { item -> item.name.isNotBlank() } + .sortedBy { it.name } + ) showContent(it.isNotEmpty()) showEmpty(it.isEmpty()) showErrorView(false) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolannouncement/SchoolAnnouncementAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolannouncement/SchoolAnnouncementAdapter.kt index 62f6251ec..46999599b 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolannouncement/SchoolAnnouncementAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolannouncement/SchoolAnnouncementAdapter.kt @@ -2,10 +2,10 @@ package io.github.wulkanowy.ui.modules.schoolannouncement import android.view.LayoutInflater import android.view.ViewGroup -import androidx.core.text.parseAsHtml import androidx.recyclerview.widget.RecyclerView import io.github.wulkanowy.data.db.entities.SchoolAnnouncement import io.github.wulkanowy.databinding.ItemSchoolAnnouncementBinding +import io.github.wulkanowy.utils.parseUonetHtml import io.github.wulkanowy.utils.toFormattedString import javax.inject.Inject @@ -28,7 +28,7 @@ class SchoolAnnouncementAdapter @Inject constructor() : with(holder.binding) { schoolAnnouncementItemDate.text = item.date.toFormattedString() schoolAnnouncementItemType.text = item.subject - schoolAnnouncementItemContent.text = item.content.parseAsHtml() + schoolAnnouncementItemContent.text = item.content.parseUonetHtml() root.setOnClickListener { onItemClickListener(item) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolannouncement/SchoolAnnouncementDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolannouncement/SchoolAnnouncementDialog.kt index 7dcd51cea..c1c584414 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolannouncement/SchoolAnnouncementDialog.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolannouncement/SchoolAnnouncementDialog.kt @@ -1,19 +1,20 @@ package io.github.wulkanowy.ui.modules.schoolannouncement +import android.app.Dialog import android.os.Bundle -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup -import androidx.core.text.parseAsHtml -import androidx.fragment.app.DialogFragment +import androidx.core.os.bundleOf +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.data.db.entities.SchoolAnnouncement import io.github.wulkanowy.databinding.DialogSchoolAnnouncementBinding -import io.github.wulkanowy.utils.lifecycleAwareVariable +import io.github.wulkanowy.ui.base.BaseDialogFragment +import io.github.wulkanowy.utils.parseUonetHtml +import io.github.wulkanowy.utils.serializable import io.github.wulkanowy.utils.toFormattedString -class SchoolAnnouncementDialog : DialogFragment() { - - private var binding: DialogSchoolAnnouncementBinding by lifecycleAwareVariable() +@AndroidEntryPoint +class SchoolAnnouncementDialog : BaseDialogFragment() { private lateinit var announcement: SchoolAnnouncement @@ -21,24 +22,24 @@ class SchoolAnnouncementDialog : DialogFragment() { private const val ARGUMENT_KEY = "item" - fun newInstance(exam: SchoolAnnouncement) = SchoolAnnouncementDialog().apply { - arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, exam) } + fun newInstance(announcement: SchoolAnnouncement) = SchoolAnnouncementDialog().apply { + arguments = bundleOf(ARGUMENT_KEY to announcement) } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setStyle(STYLE_NO_TITLE, 0) - arguments?.run { - announcement = getSerializable(ARGUMENT_KEY) as SchoolAnnouncement - } + announcement = requireArguments().serializable(ARGUMENT_KEY) } - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ) = DialogSchoolAnnouncementBinding.inflate(inflater).also { binding = it }.root + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return MaterialAlertDialogBuilder(requireContext(), theme) + .setView( + DialogSchoolAnnouncementBinding.inflate(layoutInflater) + .apply { binding = this }.root + ) + .create() + } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -46,7 +47,7 @@ class SchoolAnnouncementDialog : DialogFragment() { with(binding) { announcementDialogSubjectValue.text = announcement.subject announcementDialogDateValue.text = announcement.date.toFormattedString() - announcementDialogDescriptionValue.text = announcement.content.parseAsHtml() + announcementDialogDescriptionValue.text = announcement.content.parseUonetHtml() announcementDialogClose.setOnClickListener { dismiss() } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsFragment.kt index d56cdfa7c..f8d1323c6 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsFragment.kt @@ -3,6 +3,7 @@ package io.github.wulkanowy.ui.modules.settings import android.os.Bundle import androidx.preference.PreferenceFragmentCompat import io.github.wulkanowy.R +import io.github.wulkanowy.ui.base.BaseActivity import io.github.wulkanowy.ui.modules.main.MainView import timber.log.Timber @@ -24,11 +25,17 @@ class SettingsFragment : PreferenceFragmentCompat(), MainView.TitledView, Settin override fun showMessage(text: String) {} - override fun showExpiredDialog() {} + override fun showExpiredCredentialsDialog() {} + + override fun onCaptchaVerificationRequired(url: String?) = Unit + + override fun showDecryptionFailedDialog() {} override fun openClearLoginView() {} override fun showErrorDetailsDialog(error: Throwable) {} override fun showChangePasswordSnackbar(redirectUrl: String) {} + + override fun showAuthDialog() {} } 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 b4ba5bc4b..3ef1a80a3 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 @@ -34,7 +34,7 @@ class AdvancedFragment : PreferenceFragmentCompat(), setPreferencesFromResource(R.xml.scheme_preferences_advanced, rootKey) } - override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) { + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { presenter.onSharedPreferenceChanged(key) } @@ -46,8 +46,16 @@ class AdvancedFragment : PreferenceFragmentCompat(), (activity as? BaseActivity<*, *>)?.showMessage(text) } - override fun showExpiredDialog() { - (activity as? BaseActivity<*, *>)?.showExpiredDialog() + override fun showExpiredCredentialsDialog() { + (activity as? BaseActivity<*, *>)?.showExpiredCredentialsDialog() + } + + override fun onCaptchaVerificationRequired(url: String?) { + (activity as? BaseActivity<*, *>)?.onCaptchaVerificationRequired(url) + } + + override fun showDecryptionFailedDialog() { + (activity as? BaseActivity<*, *>)?.showDecryptionFailedDialog() } override fun showChangePasswordSnackbar(redirectUrl: String) { @@ -62,6 +70,10 @@ class AdvancedFragment : PreferenceFragmentCompat(), ErrorDialog.newInstance(error).show(childFragmentManager, error.toString()) } + override fun showAuthDialog() { + (activity as? BaseActivity<*, *>)?.showAuthDialog() + } + override fun onResume() { super.onResume() preferenceScreen.sharedPreferences?.registerOnSharedPreferenceChangeListener(this) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/advanced/AdvancedPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/advanced/AdvancedPresenter.kt index d38f841fe..4bc24594a 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/advanced/AdvancedPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/advanced/AdvancedPresenter.kt @@ -18,7 +18,8 @@ class AdvancedPresenter @Inject constructor( Timber.i("Settings advanced view was initialized") } - fun onSharedPreferenceChanged(key: String) { + fun onSharedPreferenceChanged(key: String?) { + key ?: return Timber.i("Change settings $key") analytics.logEvent("setting_changed", "name" to key) } 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 1f6d5143b..3d0c8052b 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 @@ -38,7 +38,7 @@ class AppearanceFragment : PreferenceFragmentCompat(), setPreferencesFromResource(R.xml.scheme_preferences_appearance, rootKey) } - override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) { + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { presenter.onSharedPreferenceChanged(key) } @@ -62,8 +62,16 @@ class AppearanceFragment : PreferenceFragmentCompat(), (activity as? BaseActivity<*, *>)?.showMessage(text) } - override fun showExpiredDialog() { - (activity as? BaseActivity<*, *>)?.showExpiredDialog() + override fun showExpiredCredentialsDialog() { + (activity as? BaseActivity<*, *>)?.showExpiredCredentialsDialog() + } + + override fun onCaptchaVerificationRequired(url: String?) { + (activity as? BaseActivity<*, *>)?.onCaptchaVerificationRequired(url) + } + + override fun showDecryptionFailedDialog() { + (activity as? BaseActivity<*, *>)?.showDecryptionFailedDialog() } override fun showChangePasswordSnackbar(redirectUrl: String) { @@ -78,6 +86,10 @@ class AppearanceFragment : PreferenceFragmentCompat(), ErrorDialog.newInstance(error).show(childFragmentManager, error.toString()) } + override fun showAuthDialog() { + (activity as? BaseActivity<*, *>)?.showAuthDialog() + } + override fun onResume() { super.onResume() preferenceScreen.sharedPreferences?.registerOnSharedPreferenceChangeListener(this) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/AppearancePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/AppearancePresenter.kt index 14592a6cc..d3410ea84 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/AppearancePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/AppearancePresenter.kt @@ -22,7 +22,8 @@ class AppearancePresenter @Inject constructor( Timber.i("Settings appearance view was initialized") } - fun onSharedPreferenceChanged(key: String) { + fun onSharedPreferenceChanged(key: String?) { + key ?: return Timber.i("Change settings $key") preferencesRepository.apply { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/menuorder/AppMenuItem.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/menuorder/AppMenuItem.kt new file mode 100644 index 000000000..f522913ac --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/menuorder/AppMenuItem.kt @@ -0,0 +1,206 @@ +package io.github.wulkanowy.ui.modules.settings.appearance.menuorder + +import io.github.wulkanowy.R +import io.github.wulkanowy.ui.modules.Destination +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient + +@Serializable +sealed class AppMenuItem { + + companion object { + val defaultAppMenuItemList = setOf( + DashboardAppMenuItem(), + GradeAppMenuItem(), + TimetableAppMenuItem(), + AttendanceAppMenuItem(), + ExamsAppMenuItem(), + HomeworkAppMenuItem(), + NoteAppMenuItem(), + LuckyNumberAppMenuItem(), + SchoolAnnouncementsAppMenuItem(), + SchoolAndTeachersAppMenuItem(), + MobileDevicesAppMenuItem(), + ConferenceAppMenuItem(), + MessageAppMenuItem() + ).sortedBy { it.order } + } + + // https://youtrack.jetbrains.com/issue/KT-38958 + abstract var order: Int + + abstract val icon: Int + + abstract val title: Int + + abstract val destinationType: Destination.Type + + @Serializable + data class DashboardAppMenuItem(override var order: Int = 0) : AppMenuItem() { + + @Transient + override val icon = R.drawable.ic_main_dashboard + + @Transient + override val title = R.string.dashboard_title + + @Transient + override val destinationType = Destination.Type.DASHBOARD + } + + @Serializable + data class GradeAppMenuItem(override var order: Int = 1) : AppMenuItem() { + + @Transient + override val icon = R.drawable.ic_main_grade + + @Transient + override val title = R.string.grade_title + + @Transient + override val destinationType = Destination.Type.GRADE + } + + @Serializable + data class AttendanceAppMenuItem(override var order: Int = 2) : AppMenuItem() { + + @Transient + override val icon = R.drawable.ic_main_attendance + + @Transient + override val title = R.string.attendance_title + + @Transient + override val destinationType = Destination.Type.ATTENDANCE + } + + @Serializable + data class TimetableAppMenuItem(override var order: Int = 3) : AppMenuItem() { + + @Transient + override val icon = R.drawable.ic_main_timetable + + @Transient + override val title = R.string.timetable_title + + @Transient + override val destinationType = Destination.Type.TIMETABLE + } + + @Serializable + data class MessageAppMenuItem(override var order: Int = 4) : AppMenuItem() { + + @Transient + override val icon = R.drawable.ic_more_messages + + @Transient + override val title = R.string.message_title + + @Transient + override val destinationType = Destination.Type.MESSAGE + } + + @Serializable + data class ExamsAppMenuItem(override var order: Int = 5) : AppMenuItem() { + + @Transient + override val icon = R.drawable.ic_main_exam + + @Transient + override val title = R.string.exam_title + + @Transient + override val destinationType = Destination.Type.EXAM + } + + @Serializable + data class HomeworkAppMenuItem(override var order: Int = 6) : AppMenuItem() { + + @Transient + override val icon = R.drawable.ic_more_homework + + @Transient + override val title = R.string.homework_title + + @Transient + override val destinationType = Destination.Type.HOMEWORK + } + + @Serializable + data class NoteAppMenuItem(override var order: Int = 7) : AppMenuItem() { + + @Transient + override val icon = R.drawable.ic_more_note + + @Transient + override val title = R.string.note_title + + @Transient + override val destinationType = Destination.Type.NOTE + } + + @Serializable + data class LuckyNumberAppMenuItem(override var order: Int = 8) : AppMenuItem() { + + @Transient + override val icon = R.drawable.ic_more_lucky_number + + @Transient + override val title = R.string.lucky_number_title + + @Transient + override val destinationType = Destination.Type.LUCKY_NUMBER + } + + @Serializable + data class ConferenceAppMenuItem(override var order: Int = 9) : AppMenuItem() { + + @Transient + override val icon = R.drawable.ic_more_conferences + + @Transient + override val title = R.string.conferences_title + + @Transient + override val destinationType = Destination.Type.CONFERENCE + } + + @Serializable + data class SchoolAnnouncementsAppMenuItem(override var order: Int = 10) : AppMenuItem() { + + @Transient + override val icon = R.drawable.ic_all_about + + @Transient + override val title = R.string.school_announcement_title + + @Transient + override val destinationType = Destination.Type.SCHOOL_ANNOUNCEMENT + } + + @Serializable + data class SchoolAndTeachersAppMenuItem(override var order: Int = 11) : AppMenuItem() { + + @Transient + override val icon = R.drawable.ic_more_schoolandteachers + + @Transient + override val title = R.string.schoolandteachers_title + + @Transient + override val destinationType = Destination.Type.SCHOOL_AND_TEACHERS + } + + @Serializable + data class MobileDevicesAppMenuItem(override var order: Int = 12) : AppMenuItem() { + + @Transient + override val icon = R.drawable.ic_more_mobile_devices + + @Transient + override val title = R.string.mobile_devices_title + + @Transient + override val destinationType = Destination.Type.MOBILE_DEVICE + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/menuorder/MenuItemMoveCallback.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/menuorder/MenuItemMoveCallback.kt new file mode 100644 index 000000000..49196fad2 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/menuorder/MenuItemMoveCallback.kt @@ -0,0 +1,44 @@ +package io.github.wulkanowy.ui.modules.settings.appearance.menuorder + +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.RecyclerView +import java.util.* + +class MenuItemMoveCallback( + private val menuOrderAdapter: MenuOrderAdapter, + private var onUserInteractionEndListener: (List) -> Unit = {} +) : ItemTouchHelper.Callback() { + + override fun isLongPressDragEnabled() = true + + override fun isItemViewSwipeEnabled() = false + + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { + //Not implemented + } + + override fun getMovementFlags( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder + ) = makeMovementFlags(ItemTouchHelper.UP or ItemTouchHelper.DOWN, 0) + + override fun onMove( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder + ): Boolean { + val list = menuOrderAdapter.items.toMutableList() + + Collections.swap(list, viewHolder.bindingAdapterPosition, target.bindingAdapterPosition) + + menuOrderAdapter.submitList(list) + return true + } + + override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) { + super.clearView(recyclerView, viewHolder) + + onUserInteractionEndListener(menuOrderAdapter.items.toList()) + } +} + diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/menuorder/MenuOrderAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/menuorder/MenuOrderAdapter.kt new file mode 100644 index 000000000..6bdd2fe0c --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/menuorder/MenuOrderAdapter.kt @@ -0,0 +1,58 @@ +package io.github.wulkanowy.ui.modules.settings.appearance.menuorder + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import io.github.wulkanowy.databinding.ItemMenuOrderBinding +import javax.inject.Inject + +class MenuOrderAdapter @Inject constructor() : + RecyclerView.Adapter() { + + val items = mutableListOf() + + fun submitList(newItems: List) { + val diffResult = DiffUtil.calculateDiff(DiffCallback(newItems, items.toMutableList())) + + with(items) { + clear() + addAll(newItems) + } + + diffResult.dispatchUpdatesTo(this) + } + + override fun getItemCount() = items.size + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder( + ItemMenuOrderBinding.inflate(LayoutInflater.from(parent.context), parent, false) + ) + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val item = items[position].appMenuItem + + with(holder.binding) { + menuOrderItemTitle.setText(item.title) + menuOrderItemIcon.setImageResource(item.icon) + } + } + + class ViewHolder(val binding: ItemMenuOrderBinding) : RecyclerView.ViewHolder(binding.root) + + private class DiffCallback( + private val oldList: List, + private val newList: List + ) : DiffUtil.Callback() { + + override fun getNewListSize() = newList.size + + override fun getOldListSize() = oldList.size + + override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) = + oldList[oldItemPosition] == newList[newItemPosition] + + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) = + oldList[oldItemPosition].appMenuItem.destinationType == newList[newItemPosition].appMenuItem.destinationType + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/menuorder/MenuOrderDividerItemDecoration.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/menuorder/MenuOrderDividerItemDecoration.kt new file mode 100644 index 000000000..21a7f5e80 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/menuorder/MenuOrderDividerItemDecoration.kt @@ -0,0 +1,50 @@ +package io.github.wulkanowy.ui.modules.settings.appearance.menuorder + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Rect +import android.graphics.drawable.ShapeDrawable +import android.view.View +import androidx.core.graphics.drawable.DrawableCompat +import androidx.core.view.forEach +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.RecyclerView +import io.github.wulkanowy.R +import io.github.wulkanowy.utils.getThemeAttrColor + +class MenuOrderDividerItemDecoration(private val context: Context) : + DividerItemDecoration(context, VERTICAL) { + + private val dividerDrawable = ShapeDrawable() + .apply { + DrawableCompat.setTint(this, context.getThemeAttrColor(R.attr.colorDivider)) + } + + override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) { + canvas.save() + val dividerLeft = parent.paddingLeft + val dividerRight = parent.width - parent.paddingRight + + parent.forEach { + if (parent.getChildAdapterPosition(it) == 3) { + val params = it.layoutParams as RecyclerView.LayoutParams + val dividerTop = it.bottom + params.bottomMargin + val dividerBottom = dividerTop + dividerDrawable.intrinsicHeight + + dividerDrawable.setBounds(dividerLeft, dividerTop, dividerRight, dividerBottom + 30) + dividerDrawable.draw(canvas) + } + } + + canvas.restore() + } + + override fun getItemOffsets( + outRect: Rect, view: View, parent: RecyclerView, + state: RecyclerView.State + ) { + if (parent.getChildAdapterPosition(view) == 3) { + outRect.bottom = dividerDrawable.intrinsicHeight + 30 + } + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/menuorder/MenuOrderFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/menuorder/MenuOrderFragment.kt new file mode 100644 index 000000000..e08fc5dd3 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/menuorder/MenuOrderFragment.kt @@ -0,0 +1,101 @@ +package io.github.wulkanowy.ui.modules.settings.appearance.menuorder + +import android.os.Bundle +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import androidx.activity.addCallback +import androidx.core.view.MenuProvider +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.SimpleItemAnimator +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import dagger.hilt.android.AndroidEntryPoint +import io.github.wulkanowy.R +import io.github.wulkanowy.databinding.FragmentMenuOrderBinding +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 javax.inject.Inject + + +@AndroidEntryPoint +class MenuOrderFragment : BaseFragment(R.layout.fragment_menu_order), + MenuOrderView, MainView.TitledView { + + @Inject + lateinit var presenter: MenuOrderPresenter + + @Inject + lateinit var menuOrderAdapter: MenuOrderAdapter + + override val titleStringId = R.string.menu_order_title + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding = FragmentMenuOrderBinding.bind(view) + presenter.onAttachView(this) + } + + override fun initView() { + val itemTouchHelper = ItemTouchHelper( + MenuItemMoveCallback(menuOrderAdapter, presenter::onDragAndDropEnd) + ) + + itemTouchHelper.attachToRecyclerView(binding.menuOrderRecycler) + + with(binding.menuOrderRecycler) { + layoutManager = LinearLayoutManager(context) + adapter = menuOrderAdapter + addItemDecoration(MenuOrderDividerItemDecoration(context)) + addItemDecoration(DividerItemDecoration(context)) + (itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false + } + + requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner) { + presenter.onBackSelected() + } + + initializeToolbar() + } + + private fun initializeToolbar() { + requireActivity().addMenuProvider(object : MenuProvider { + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean { + if (menuItem.itemId == android.R.id.home) { + presenter.onBackSelected() + return true + } + return false + } + + }, viewLifecycleOwner) + } + + override fun updateData(data: List) { + menuOrderAdapter.submitList(data) + } + + override fun restartApp() { + startActivity(MainActivity.getStartIntent(requireContext())) + requireActivity().finishAffinity() + } + + override fun popView() { + (activity as? MainActivity?)?.popView() + } + + override fun showRestartConfirmationDialog() { + MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.menu_order_confirm_title) + .setMessage(R.string.menu_order_confirm_content) + .setPositiveButton(R.string.menu_order_confirm_restart) { _, _ -> presenter.onConfirmRestart() } + .setNegativeButton(R.string.all_cancel) { _, _ -> presenter.onCancelRestart() } + .show() + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/menuorder/MenuOrderItem.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/menuorder/MenuOrderItem.kt new file mode 100644 index 000000000..b82bc1bd2 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/menuorder/MenuOrderItem.kt @@ -0,0 +1,6 @@ +package io.github.wulkanowy.ui.modules.settings.appearance.menuorder + +data class MenuOrderItem( + val appMenuItem: AppMenuItem, + val order: Int +) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/menuorder/MenuOrderPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/menuorder/MenuOrderPresenter.kt new file mode 100644 index 000000000..1c90cc5e6 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/menuorder/MenuOrderPresenter.kt @@ -0,0 +1,64 @@ +package io.github.wulkanowy.ui.modules.settings.appearance.menuorder + +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 timber.log.Timber +import javax.inject.Inject + +class MenuOrderPresenter @Inject constructor( + studentRepository: StudentRepository, + errorHandler: ErrorHandler, + private val preferencesRepository: PreferencesRepository +) : BasePresenter(errorHandler, studentRepository) { + + private var updatedMenuOrderItems = emptyList() + + override fun onAttachView(view: MenuOrderView) { + super.onAttachView(view) + view.initView() + Timber.i("Menu order view was initialized") + loadData() + } + + private fun loadData() { + val savedMenuItemList = (preferencesRepository.appMenuItemOrder) + .sortedBy { it.order } + .map { MenuOrderItem(it, it.order) } + + view?.updateData(savedMenuItemList) + } + + fun onDragAndDropEnd(list: List) { + val updatedList = list.mapIndexed { index, menuOrderItem -> + menuOrderItem.copy(order = index) + } + + updatedMenuOrderItems = updatedList + view?.updateData(updatedList) + } + + fun onBackSelected() { + if (updatedMenuOrderItems.isNotEmpty()) { + view?.showRestartConfirmationDialog() + } else { + view?.popView() + } + } + + fun onConfirmRestart() { + updatedMenuOrderItems.forEach { + it.appMenuItem.apply { + order = it.order + } + } + + preferencesRepository.appMenuItemOrder = updatedMenuOrderItems.map { it.appMenuItem } + view?.restartApp() + } + + fun onCancelRestart() { + view?.popView() + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/menuorder/MenuOrderView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/menuorder/MenuOrderView.kt new file mode 100644 index 000000000..264e68ccb --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/menuorder/MenuOrderView.kt @@ -0,0 +1,16 @@ +package io.github.wulkanowy.ui.modules.settings.appearance.menuorder + +import io.github.wulkanowy.ui.base.BaseView + +interface MenuOrderView : BaseView { + + fun initView() + + fun updateData(data: List) + + fun restartApp() + + fun showRestartConfirmationDialog() + + fun popView() +} 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 364ad2137..0bf9ddadd 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsFragment.kt @@ -1,22 +1,20 @@ package io.github.wulkanowy.ui.modules.settings.notifications -import android.annotation.SuppressLint import android.content.Intent import android.content.SharedPreferences -import android.net.Uri -import android.os.Build +import android.content.pm.PackageManager import android.os.Bundle -import android.provider.Settings import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.activity.result.contract.ActivityResultContracts -import androidx.appcompat.app.AlertDialog import androidx.core.app.NotificationManagerCompat +import androidx.core.content.ContextCompat import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import androidx.preference.SwitchPreferenceCompat import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.thelittlefireman.appkillermanager.AppKillerManager import com.thelittlefireman.appkillermanager.exceptions.NoActionFoundException import dagger.hilt.android.AndroidEntryPoint @@ -26,7 +24,7 @@ import io.github.wulkanowy.ui.base.ErrorDialog import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.openInternetBrowser -import timber.log.Timber +import io.github.wulkanowy.utils.openNotificationSettings import javax.inject.Inject @AndroidEntryPoint @@ -42,7 +40,14 @@ class NotificationsFragment : PreferenceFragmentCompat(), override val titleStringId get() = R.string.pref_settings_notifications_title + private val notificationsPermission = "android.permission.POST_NOTIFICATIONS" + override val isNotificationPermissionGranted: Boolean + get() = ContextCompat.checkSelfPermission( + requireContext(), notificationsPermission + ) == PackageManager.PERMISSION_GRANTED + + override val isNotificationPiggybackPermissionGranted: Boolean get() { val packageNameList = NotificationManagerCompat.getEnabledListenerPackages(requireContext()) @@ -51,6 +56,13 @@ class NotificationsFragment : PreferenceFragmentCompat(), return appPackageName in packageNameList } + private val requestPermissionLauncher = + registerForActivityResult(ActivityResultContracts.RequestPermission()) { + if (it) { + presenter.onNotificationsPermissionResult() + } else openNotificationsPermissionDialog() + } + private val notificationSettingsPiggybackContract = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { presenter.onNotificationPiggybackPermissionResult() @@ -101,7 +113,7 @@ class NotificationsFragment : PreferenceFragmentCompat(), setPreferencesFromResource(R.xml.scheme_preferences_notifications, rootKey) } - override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) { + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { presenter.onSharedPreferenceChanged(key) } @@ -120,8 +132,16 @@ class NotificationsFragment : PreferenceFragmentCompat(), (activity as? BaseActivity<*, *>)?.showMessage(text) } - override fun showExpiredDialog() { - (activity as? BaseActivity<*, *>)?.showExpiredDialog() + override fun showExpiredCredentialsDialog() { + (activity as? BaseActivity<*, *>)?.showExpiredCredentialsDialog() + } + + override fun onCaptchaVerificationRequired(url: String?) { + (activity as? BaseActivity<*, *>)?.onCaptchaVerificationRequired(url) + } + + override fun showDecryptionFailedDialog() { + (activity as? BaseActivity<*, *>)?.showDecryptionFailedDialog() } override fun showChangePasswordSnackbar(redirectUrl: String) { @@ -136,8 +156,12 @@ class NotificationsFragment : PreferenceFragmentCompat(), ErrorDialog.newInstance(error).show(childFragmentManager, error.toString()) } + override fun showAuthDialog() { + (activity as? BaseActivity<*, *>)?.showAuthDialog() + } + override fun showFixSyncDialog() { - AlertDialog.Builder(requireContext()) + MaterialAlertDialogBuilder(requireContext()) .setTitle(R.string.pref_notify_fix_sync_issues) .setMessage(R.string.pref_notify_fix_sync_issues_message) .setNegativeButton(android.R.string.cancel) { _, _ -> } @@ -156,26 +180,30 @@ class NotificationsFragment : PreferenceFragmentCompat(), .show() } - @SuppressLint("InlinedApi") override fun openSystemSettings() { - val intent = if (appInfo.systemVersion >= Build.VERSION_CODES.O) { - Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply { - putExtra("android.provider.extra.APP_PACKAGE", requireActivity().packageName) - } - } else { - Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { - data = Uri.fromParts("package", requireActivity().packageName, null) - } - } - try { - requireActivity().startActivity(intent) - } catch (e: Exception) { - Timber.e(e) - } + requireActivity().openNotificationSettings() } - override fun openNotificationPermissionDialog() { - AlertDialog.Builder(requireContext()) + override fun requestNotificationPermissions() { + requestPermissionLauncher.launch(notificationsPermission) + } + + override fun openNotificationsPermissionDialog() { + MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.notifications_header_title) + .setMessage(R.string.notifications_header_description) + .setPositiveButton(R.string.pref_notification_go_to_settings) { _, _ -> + requireActivity().openNotificationSettings() + } + .setNegativeButton(android.R.string.cancel) { _, _ -> + setNotificationPreferencesChecked(false) + } + .setOnDismissListener { setNotificationPreferencesChecked(false) } + .show() + } + + override fun openNotificationPiggyBackPermissionDialog() { + MaterialAlertDialogBuilder(requireContext()) .setTitle(getString(R.string.pref_notification_piggyback_popup_title)) .setMessage(getString(R.string.pref_notification_piggyback_popup_description)) .setPositiveButton(getString(R.string.pref_notification_go_to_settings)) { _, _ -> @@ -189,7 +217,7 @@ class NotificationsFragment : PreferenceFragmentCompat(), } override fun openNotificationExactAlarmSettings() { - AlertDialog.Builder(requireContext()) + MaterialAlertDialogBuilder(requireContext()) .setTitle(getString(R.string.pref_notification_exact_alarm_popup_title)) .setMessage(getString(R.string.pref_notification_exact_alarm_popup_descriptions)) .setPositiveButton(getString(R.string.pref_notification_go_to_settings)) { _, _ -> @@ -202,6 +230,11 @@ class NotificationsFragment : PreferenceFragmentCompat(), .show() } + override fun setNotificationPreferencesChecked(isChecked: Boolean) { + findPreference(getString(R.string.pref_key_notifications_enable))?.isChecked = + isChecked + } + override fun setNotificationPiggybackPreferenceChecked(isChecked: Boolean) { findPreference(getString(R.string.pref_key_notifications_piggyback))?.isChecked = isChecked diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsPresenter.kt index 4cbdac945..b2938cf65 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsPresenter.kt @@ -26,18 +26,20 @@ class NotificationsPresenter @Inject constructor( with(view) { enableNotification( - preferencesRepository.notificationsEnableKey, - preferencesRepository.isServiceEnabled + notificationKey = preferencesRepository.notificationsEnableKey, + enable = preferencesRepository.isServiceEnabled ) initView(appInfo.isDebug) } + checkNotificationsPermissionState() checkNotificationPiggybackState() Timber.i("Settings notifications view was initialized") } - fun onSharedPreferenceChanged(key: String) { + fun onSharedPreferenceChanged(key: String?) { + key ?: return Timber.i("Change settings $key") preferencesRepository.apply { @@ -49,12 +51,17 @@ class NotificationsPresenter @Inject constructor( view?.openNotificationExactAlarmSettings() } } + notificationsEnableKey -> { + if (isNotificationsEnable && view?.isNotificationPermissionGranted == false) { + view?.requestNotificationPermissions() + } + } isDebugNotificationEnableKey -> { chuckerCollector.showNotification = isDebugNotificationEnable } isNotificationPiggybackEnabledKey -> { - if (isNotificationPiggybackEnabled && view?.isNotificationPermissionGranted == false) { - view?.openNotificationPermissionDialog() + if (isNotificationPiggybackEnabled && view?.isNotificationPiggybackPermissionGranted == false) { + view?.openNotificationPiggyBackPermissionDialog() } } } @@ -70,9 +77,15 @@ class NotificationsPresenter @Inject constructor( view?.openSystemSettings() } + fun onNotificationsPermissionResult() { + view?.run { + setNotificationPreferencesChecked(isNotificationPermissionGranted) + } + } + fun onNotificationPiggybackPermissionResult() { view?.run { - setNotificationPiggybackPreferenceChecked(isNotificationPermissionGranted) + setNotificationPiggybackPreferenceChecked(isNotificationPiggybackPermissionGranted) } } @@ -80,10 +93,18 @@ class NotificationsPresenter @Inject constructor( view?.setUpcomingLessonsNotificationPreferenceChecked(timetableNotificationHelper.canScheduleExactAlarms()) } + private fun checkNotificationsPermissionState() { + if (preferencesRepository.isNotificationsEnable) { + view?.run { + setNotificationPreferencesChecked(isNotificationPermissionGranted) + } + } + } + private fun checkNotificationPiggybackState() { if (preferencesRepository.isNotificationPiggybackEnabled) { view?.run { - setNotificationPiggybackPreferenceChecked(isNotificationPermissionGranted) + setNotificationPiggybackPreferenceChecked(isNotificationPiggybackPermissionGranted) } } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsView.kt index 2bf8e31f4..a391681cb 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsView.kt @@ -6,6 +6,8 @@ interface NotificationsView : BaseView { val isNotificationPermissionGranted: Boolean + val isNotificationPiggybackPermissionGranted: Boolean + fun initView(showDebugNotificationSwitch: Boolean) fun showFixSyncDialog() @@ -14,10 +16,16 @@ interface NotificationsView : BaseView { fun enableNotification(notificationKey: String, enable: Boolean) - fun openNotificationPermissionDialog() + fun requestNotificationPermissions() + + fun openNotificationsPermissionDialog() + + fun openNotificationPiggyBackPermissionDialog() fun openNotificationExactAlarmSettings() + fun setNotificationPreferencesChecked(isChecked: Boolean) + fun setNotificationPiggybackPreferenceChecked(isChecked: Boolean) fun setUpcomingLessonsNotificationPreferenceChecked(isChecked: Boolean) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncFragment.kt index 8477e3222..d57144832 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 @@ -5,6 +5,7 @@ import android.os.Bundle import android.view.View import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat +import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.ui.base.BaseActivity @@ -50,7 +51,7 @@ class SyncFragment : PreferenceFragmentCompat(), setPreferencesFromResource(R.xml.scheme_preferences_sync, rootKey) } - override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) { + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { presenter.onSharedPreferenceChanged(key) } @@ -75,11 +76,23 @@ class SyncFragment : PreferenceFragmentCompat(), } override fun showMessage(text: String) { - (activity as? BaseActivity<*, *>)?.showMessage(text) + Snackbar.make(requireView(), text, Snackbar.LENGTH_LONG) + .apply { + anchorView = requireActivity().findViewById(R.id.main_bottom_nav) + show() + } } - override fun showExpiredDialog() { - (activity as? BaseActivity<*, *>)?.showExpiredDialog() + override fun showExpiredCredentialsDialog() { + (activity as? BaseActivity<*, *>)?.showExpiredCredentialsDialog() + } + + override fun onCaptchaVerificationRequired(url: String?) { + (activity as? BaseActivity<*, *>)?.onCaptchaVerificationRequired(url) + } + + override fun showDecryptionFailedDialog() { + (activity as? BaseActivity<*, *>)?.showDecryptionFailedDialog() } override fun showChangePasswordSnackbar(redirectUrl: String) { @@ -94,6 +107,10 @@ class SyncFragment : PreferenceFragmentCompat(), ErrorDialog.newInstance(error).show(childFragmentManager, "error_details") } + override fun showAuthDialog() { + (activity as? BaseActivity<*, *>)?.showAuthDialog() + } + override fun onResume() { super.onResume() preferenceScreen.sharedPreferences?.registerOnSharedPreferenceChangeListener(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 1ecb4a6ef..594e097af 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 @@ -31,7 +31,8 @@ class SyncPresenter @Inject constructor( setSyncDateInView() } - fun onSharedPreferenceChanged(key: String) { + fun onSharedPreferenceChanged(key: String?) { + key ?: return Timber.i("Change settings $key") preferencesRepository.apply { @@ -52,10 +53,12 @@ class SyncPresenter @Inject constructor( Timber.i("Setting sync now started") analytics.logEvent("sync_now", "status" to "started") } + WorkInfo.State.SUCCEEDED -> { showMessage(syncSuccessString) analytics.logEvent("sync_now", "status" to "success") } + WorkInfo.State.FAILED -> { showError( syncFailedString, @@ -66,6 +69,7 @@ class SyncPresenter @Inject constructor( ) analytics.logEvent("sync_now", "status" to "failed") } + else -> Timber.d("Sync now state: ${workInfo?.state}") } if (workInfo?.state?.isFinished == true) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/studentinfo/StudentInfoFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/studentinfo/StudentInfoFragment.kt index 361a59440..598046a2d 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/studentinfo/StudentInfoFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/studentinfo/StudentInfoFragment.kt @@ -8,6 +8,7 @@ import android.view.MenuInflater import android.view.View import android.widget.Toast import androidx.core.content.getSystemService +import androidx.core.os.bundleOf import androidx.core.view.get import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager @@ -24,6 +25,8 @@ import io.github.wulkanowy.ui.modules.main.MainActivity import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.utils.capitalise import io.github.wulkanowy.utils.getThemeAttrColor +import io.github.wulkanowy.utils.nullableSerializable +import io.github.wulkanowy.utils.serializable import javax.inject.Inject @AndroidEntryPoint @@ -38,7 +41,9 @@ class StudentInfoFragment : lateinit var studentInfoAdapter: StudentInfoAdapter override val titleStringId: Int - get() = when (requireArguments().getSerializable(INFO_TYPE_ARGUMENT_KEY) as? StudentInfoView.Type) { + get() = when ( + requireArguments().nullableSerializable(INFO_TYPE_ARGUMENT_KEY) + ) { StudentInfoView.Type.PERSONAL -> R.string.account_personal_data StudentInfoView.Type.CONTACT -> R.string.account_contact StudentInfoView.Type.ADDRESS -> R.string.account_address @@ -58,13 +63,14 @@ class StudentInfoFragment : fun newInstance(type: StudentInfoView.Type, studentWithSemesters: StudentWithSemesters) = StudentInfoFragment().apply { - arguments = Bundle().apply { - putSerializable(INFO_TYPE_ARGUMENT_KEY, type) - putSerializable(STUDENT_ARGUMENT_KEY, studentWithSemesters) - } + arguments = bundleOf( + INFO_TYPE_ARGUMENT_KEY to type, + STUDENT_ARGUMENT_KEY to studentWithSemesters + ) } } + @Suppress("DEPRECATION") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setHasOptionsMenu(true) @@ -74,9 +80,9 @@ class StudentInfoFragment : super.onViewCreated(view, savedInstanceState) binding = FragmentStudentInfoBinding.bind(view) presenter.onAttachView( - this, - requireArguments().getSerializable(INFO_TYPE_ARGUMENT_KEY) as StudentInfoView.Type, - requireArguments().getSerializable(STUDENT_ARGUMENT_KEY) as StudentWithSemesters + view = this, + type = requireArguments().serializable(INFO_TYPE_ARGUMENT_KEY), + studentWithSemesters = requireArguments().serializable(STUDENT_ARGUMENT_KEY), ) } @@ -153,7 +159,6 @@ class StudentInfoFragment : ) } - @OptIn(ExperimentalStdlibApi::class) override fun showFamilyTypeData(studentInfo: StudentInfo) { val items = buildList { add(studentInfo.firstGuardian?.let { 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 d6917672a..a957ef798 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 @@ -12,7 +12,9 @@ import androidx.recyclerview.widget.RecyclerView import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Timetable import io.github.wulkanowy.databinding.ItemTimetableBinding +import io.github.wulkanowy.databinding.ItemTimetableEmptyBinding import io.github.wulkanowy.databinding.ItemTimetableSmallBinding +import io.github.wulkanowy.utils.getPlural import io.github.wulkanowy.utils.getThemeAttrColor import io.github.wulkanowy.utils.toFormattedString import javax.inject.Inject @@ -29,9 +31,14 @@ class TimetableAdapter @Inject constructor() : TimetableItemType.SMALL -> SmallViewHolder( ItemTimetableSmallBinding.inflate(inflater, parent, false) ) + TimetableItemType.NORMAL -> NormalViewHolder( ItemTimetableBinding.inflate(inflater, parent, false) ) + + TimetableItemType.EMPTY -> EmptyViewHolder( + ItemTimetableEmptyBinding.inflate(inflater, parent, false) + ) } } @@ -40,12 +47,12 @@ class TimetableAdapter @Inject constructor() : 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, - ) + if (payloads.isNotEmpty() && holder is NormalViewHolder) { + updateTimeLeft( + binding = holder.binding, + timeLeft = (getItem(position) as TimetableItem.Normal).timeLeft, + ) + } else super.onBindViewHolder(holder, position, payloads) } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { @@ -54,10 +61,16 @@ class TimetableAdapter @Inject constructor() : binding = holder.binding, item = getItem(position) as TimetableItem.Small, ) + is NormalViewHolder -> bindNormalView( binding = holder.binding, item = getItem(position) as TimetableItem.Normal, ) + + is EmptyViewHolder -> bindEmptyView( + binding = holder.binding, + item = getItem(position) as TimetableItem.Empty, + ) } } @@ -100,6 +113,19 @@ class TimetableAdapter @Inject constructor() : } } + private fun bindEmptyView(binding: ItemTimetableEmptyBinding, item: TimetableItem.Empty) { + with(binding) { + timetableEmptyItemNumber.text = when (item.numFrom) { + item.numTo -> item.numFrom.toString() + else -> "${item.numFrom}-${item.numTo}" + } + timetableEmptyItemSubject.text = timetableEmptyItemSubject.context.getPlural( + R.plurals.timetable_no_lesson, + item.numTo - item.numFrom + 1 + ) + } + } + private fun updateTimeLeft(binding: ItemTimetableBinding, timeLeft: TimeLeft?) { with(binding) { when { @@ -137,6 +163,7 @@ class TimetableAdapter @Inject constructor() : timetableItemTimeLeft.visibility = VISIBLE timetableItemTimeLeft.text = root.context.getString(R.string.timetable_finished) } + else -> { timetableItemTimeUntil.visibility = GONE timetableItemTimeLeft.visibility = GONE @@ -160,7 +187,7 @@ class TimetableAdapter @Inject constructor() : timetableSmallItemDescription.setTextColor( root.context.getThemeAttrColor( - if (lesson.canceled) R.attr.colorPrimary + if (lesson.canceled) R.attr.colorTimetableCanceled else R.attr.colorTimetableChange ) ) @@ -185,13 +212,14 @@ class TimetableAdapter @Inject constructor() : timetableItemDescription.setTextColor( root.context.getThemeAttrColor( - if (lesson.canceled) R.attr.colorPrimary + if (lesson.canceled) R.attr.colorTimetableCanceled else R.attr.colorTimetableChange ) ) } else { timetableItemDescription.visibility = GONE - timetableItemRoom.visibility = VISIBLE + timetableItemRoom.isVisible = + lesson.room.isNotBlank() || lesson.roomOld.isNotBlank() timetableItemGroup.isVisible = item.showGroupsInPlan && lesson.group.isNotBlank() timetableItemTeacher.visibility = VISIBLE } @@ -228,8 +256,8 @@ class TimetableAdapter @Inject constructor() : } private fun updateNumberAndSubjectCanceledColor(numberView: TextView, subjectView: TextView) { - numberView.setTextColor(numberView.context.getThemeAttrColor(R.attr.colorPrimary)) - subjectView.setTextColor(subjectView.context.getThemeAttrColor(R.attr.colorPrimary)) + numberView.setTextColor(numberView.context.getThemeAttrColor(R.attr.colorTimetableCanceled)) + subjectView.setTextColor(subjectView.context.getThemeAttrColor(R.attr.colorTimetableCanceled)) } private fun updateNumberColor(numberView: TextView, lesson: Timetable) { @@ -274,6 +302,9 @@ class TimetableAdapter @Inject constructor() : private class SmallViewHolder(val binding: ItemTimetableSmallBinding) : RecyclerView.ViewHolder(binding.root) + private class EmptyViewHolder(val binding: ItemTimetableEmptyBinding) : + RecyclerView.ViewHolder(binding.root) + companion object { private val differ = object : DiffUtil.ItemCallback() { override fun areItemsTheSame(oldItem: TimetableItem, newItem: TimetableItem): Boolean = @@ -281,9 +312,11 @@ class TimetableAdapter @Inject constructor() : 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 } @@ -292,7 +325,7 @@ class TimetableAdapter @Inject constructor() : 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) { + if (oldItem.lesson == newItem.lesson && oldItem.showGroupsInPlan == newItem.showGroupsInPlan && oldItem.timeLeft != newItem.timeLeft) { "time_left" } else super.getChangePayload(oldItem, newItem) } else super.getChangePayload(oldItem, newItem) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableDialog.kt index c9243b12e..e8a853479 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 @@ -1,26 +1,24 @@ package io.github.wulkanowy.ui.modules.timetable import android.annotation.SuppressLint +import android.app.Dialog import android.graphics.Paint.STRIKE_THRU_TEXT_FLAG import android.os.Bundle -import android.view.LayoutInflater import android.view.View import android.view.View.GONE import android.view.View.VISIBLE -import android.view.ViewGroup -import androidx.fragment.app.DialogFragment +import androidx.core.os.bundleOf +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Timetable import io.github.wulkanowy.databinding.DialogTimetableBinding -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 io.github.wulkanowy.ui.base.BaseDialogFragment +import io.github.wulkanowy.utils.* import java.time.Instant -class TimetableDialog : DialogFragment() { - - private var binding: DialogTimetableBinding by lifecycleAwareVariable() +@AndroidEntryPoint +class TimetableDialog : BaseDialogFragment() { private lateinit var lesson: Timetable @@ -28,24 +26,21 @@ class TimetableDialog : DialogFragment() { private const val ARGUMENT_KEY = "Item" - fun newInstance(exam: Timetable) = TimetableDialog().apply { - arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, exam) } + fun newInstance(lesson: Timetable) = TimetableDialog().apply { + arguments = bundleOf(ARGUMENT_KEY to lesson) } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setStyle(STYLE_NO_TITLE, 0) - arguments?.run { - lesson = getSerializable(ARGUMENT_KEY) as Timetable - } + lesson = requireArguments().serializable(ARGUMENT_KEY) } - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ) = DialogTimetableBinding.inflate(inflater).apply { binding = this }.root + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return MaterialAlertDialogBuilder(requireContext(), theme) + .setView(DialogTimetableBinding.inflate(layoutInflater).apply { binding = this }.root) + .create() + } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -86,12 +81,12 @@ class TimetableDialog : DialogFragment() { if (canceled) { timetableDialogChangesTitle.setTextColor( requireContext().getThemeAttrColor( - R.attr.colorPrimary + R.attr.colorTimetableCanceled ) ) timetableDialogChangesValue.setTextColor( requireContext().getThemeAttrColor( - R.attr.colorPrimary + R.attr.colorTimetableCanceled ) ) } else { 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 fdd4aface..0e6459110 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 @@ -7,7 +7,9 @@ import android.view.MenuItem import android.view.View import android.view.View.GONE import android.view.View.VISIBLE +import androidx.core.os.bundleOf import androidx.core.text.parseAsHtml +import androidx.core.view.isVisible import androidx.recyclerview.widget.LinearLayoutManager import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R @@ -39,9 +41,7 @@ class TimetableFragment : BaseFragment(R.layout.fragme private const val ARGUMENT_DATE_KEY = "ARGUMENT_DATE" fun newInstance(date: LocalDate? = null) = TimetableFragment().apply { - arguments = Bundle().apply { - date?.let { putLong(ARGUMENT_DATE_KEY, it.toEpochDay()) } - } + arguments = date?.let { bundleOf(ARGUMENT_DATE_KEY to it.toEpochDay()) } } } @@ -51,6 +51,7 @@ class TimetableFragment : BaseFragment(R.layout.fragme override val currentStackSize get() = (activity as? MainActivity)?.currentStackSize + @Suppress("DEPRECATION") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setHasOptionsMenu(true) @@ -87,7 +88,7 @@ class TimetableFragment : BaseFragment(R.layout.fragme timetableNavDate.setOnClickListener { presenter.onPickDate() } timetableNextButton.setOnClickListener { presenter.onNextDay() } - timetableNavContainer.elevation = requireContext().dpToPx(8f) + timetableNavContainer.elevation = requireContext().dpToPx(3f) } } @@ -160,6 +161,10 @@ class TimetableFragment : BaseFragment(R.layout.fragme binding.timetableRecycler.visibility = if (show) VISIBLE else GONE } + override fun showNavigation(show: Boolean) { + binding.timetableNavContainer.isVisible = true + } + override fun showPreButton(show: Boolean) { binding.timetablePreviousButton.visibility = if (show) VISIBLE else View.INVISIBLE } @@ -193,7 +198,9 @@ class TimetableFragment : BaseFragment(R.layout.fragme override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) - outState.putLong(SAVED_DATE_KEY, presenter.currentDate.toEpochDay()) + presenter.currentDate?.toEpochDay()?.let { + outState.putLong(SAVED_DATE_KEY, it) + } } override fun 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 index 92716ace8..105ece38c 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableItem.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableItem.kt @@ -16,6 +16,11 @@ sealed class TimetableItem(val type: TimetableItemType) { val timeLeft: TimeLeft?, val onClick: (Timetable) -> Unit, ) : TimetableItem(TimetableItemType.NORMAL) + + data class Empty( + val numFrom: Int, + val numTo: Int + ) : TimetableItem(TimetableItemType.EMPTY) } data class TimeLeft( @@ -27,4 +32,5 @@ data class TimeLeft( enum class TimetableItemType { SMALL, NORMAL, + EMPTY } 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 d06874082..7e8c876ef 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,23 +1,47 @@ package io.github.wulkanowy.ui.modules.timetable -import io.github.wulkanowy.data.* +import android.os.Handler +import android.os.Looper +import io.github.wulkanowy.data.db.entities.Semester +import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Timetable +import io.github.wulkanowy.data.enums.TimetableGapsMode.BETWEEN_AND_BEFORE_LESSONS +import io.github.wulkanowy.data.enums.TimetableGapsMode.NO_GAPS import io.github.wulkanowy.data.enums.TimetableMode +import io.github.wulkanowy.data.flatResourceFlow +import io.github.wulkanowy.data.logResourceStatus +import io.github.wulkanowy.data.onResourceData +import io.github.wulkanowy.data.onResourceError +import io.github.wulkanowy.data.onResourceIntermediate +import io.github.wulkanowy.data.onResourceNotLoading +import io.github.wulkanowy.data.onResourceSuccess import io.github.wulkanowy.data.repositories.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.domain.timetable.IsStudentHasLessonsOnWeekendUseCase +import io.github.wulkanowy.domain.timetable.IsWeekendHasLessonsUseCase import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.ErrorHandler -import io.github.wulkanowy.utils.* -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.onEach +import io.github.wulkanowy.utils.AnalyticsHelper +import io.github.wulkanowy.utils.capitalise +import io.github.wulkanowy.utils.getLastSchoolDayIfHoliday +import io.github.wulkanowy.utils.isHolidays +import io.github.wulkanowy.utils.isJustFinished +import io.github.wulkanowy.utils.isShowTimeUntil +import io.github.wulkanowy.utils.left +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.until import timber.log.Timber import java.time.Instant import java.time.LocalDate -import java.time.LocalDate.* -import java.util.* +import java.time.LocalDate.now +import java.time.LocalDate.of +import java.time.LocalDate.ofEpochDay +import java.util.Timer import javax.inject.Inject import kotlin.concurrent.timer @@ -25,14 +49,17 @@ class TimetablePresenter @Inject constructor( errorHandler: ErrorHandler, studentRepository: StudentRepository, private val timetableRepository: TimetableRepository, + private val isStudentHasLessonsOnWeekendUseCase: IsStudentHasLessonsOnWeekendUseCase, + private val isWeekendHasLessonsUseCase: IsWeekendHasLessonsUseCase, private val semesterRepository: SemesterRepository, private val prefRepository: PreferencesRepository, private val analytics: AnalyticsHelper, ) : BasePresenter(errorHandler, studentRepository) { - private var baseDate: LocalDate = now().nextOrSameSchoolDay + private var initialDate: LocalDate? = null + private var isWeekendHasLessons: Boolean = false - lateinit var currentDate: LocalDate + var currentDate: LocalDate? = null private set private lateinit var lastError: Throwable @@ -44,23 +71,30 @@ class TimetablePresenter @Inject constructor( view.initView() Timber.i("Timetable was initialized") errorHandler.showErrorMessage = ::showErrorViewOnError - reloadView(ofEpochDay(date ?: baseDate.toEpochDay())) + currentDate = date?.let(::ofEpochDay) loadData() - if (currentDate.isHolidays) setBaseDateOnHolidays() } fun onPreviousDay() { - reloadView(currentDate.previousSchoolDay) + val date = if (isWeekendHasLessons) { + currentDate?.minusDays(1) + } else currentDate?.previousSchoolDay + + reloadView(date ?: return) loadData() } fun onNextDay() { - reloadView(currentDate.nextSchoolDay) + val date = if (isWeekendHasLessons) { + currentDate?.plusDays(1) + } else currentDate?.nextSchoolDay + + reloadView(date ?: return) loadData() } fun onPickDate() { - view?.showDatePickerDialog(currentDate) + view?.showDatePickerDialog(currentDate ?: return) } fun onDateSet(year: Int, month: Int, day: Int) { @@ -89,10 +123,8 @@ class TimetablePresenter @Inject constructor( Timber.i("Timetable view is reselected") view?.let { view -> if (view.currentStackSize == 1) { - baseDate = now().nextOrSameSchoolDay - - if (currentDate != baseDate) { - reloadView(baseDate) + if (currentDate != initialDate) { + reloadView(initialDate ?: return) loadData() } else if (!view.isViewEmpty) { view.resetView() @@ -113,34 +145,25 @@ class TimetablePresenter @Inject constructor( return true } - private fun setBaseDateOnHolidays() { - 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") - } - private fun loadData(forceRefresh: Boolean = false) { flatResourceFlow { val student = studentRepository.getCurrentStudent() val semester = semesterRepository.getCurrentSemester(student) + + checkInitialAndCurrentDate(student, semester) timetableRepository.getTimetable( student = student, semester = semester, - start = currentDate, - end = currentDate, + start = currentDate ?: now(), + end = currentDate ?: now(), forceRefresh = forceRefresh, timetableType = TimetableRepository.TimetableType.NORMAL ) } .logResourceStatus("load timetable data") .onResourceData { + isWeekendHasLessons = isWeekendHasLessons || isWeekendHasLessonsUseCase(it.lessons) + view?.run { enableSwipe(true) showProgress(false) @@ -148,7 +171,8 @@ class TimetablePresenter @Inject constructor( showContent(it.lessons.isNotEmpty()) showEmpty(it.lessons.isEmpty()) updateData(it.lessons) - setDayHeaderMessage(it.headers.singleOrNull { header -> header.date == currentDate }?.content) + setDayHeaderMessage(it.headers.find { header -> header.date == currentDate }?.content) + reloadNavigation() } } .onResourceIntermediate { view?.showRefresh(true) } @@ -170,14 +194,37 @@ class TimetablePresenter @Inject constructor( .launch() } + private suspend fun checkInitialAndCurrentDate(student: Student, semester: Semester) { + if (initialDate == null) { + isWeekendHasLessons = isStudentHasLessonsOnWeekendUseCase(student, semester) + initialDate = getInitialDate(semester) + } + + if (currentDate == null) { + currentDate = initialDate + } + } + + private fun getInitialDate(semester: Semester): LocalDate { + val now = now() + + return when { + now.isHolidays -> now.getLastSchoolDayIfHoliday(semester.schoolYear) + isWeekendHasLessons -> now + else -> now.nextOrSameSchoolDay + } + } + private fun updateData(lessons: List) { tickTimer?.cancel() - if (!prefRepository.showTimetableTimers) { + if (currentDate != now()) { view?.updateData(createItems(lessons)) } else { tickTimer = timer(period = 2_000) { - view?.updateData(createItems(lessons)) + Handler(Looper.getMainLooper()).post { + view?.updateData(createItems(lessons)) + } } } } @@ -192,16 +239,38 @@ class TimetablePresenter @Inject constructor( 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 - ) + var prevNum = when (prefRepository.showTimetableGaps) { + BETWEEN_AND_BEFORE_LESSONS -> 0 + else -> null + } + return buildList { + filteredItems.forEachIndexed { i, it -> + if (prefRepository.showTimetableGaps != NO_GAPS && prevNum != null && it.number > prevNum!! + 1) { + val emptyLesson = TimetableItem.Empty( + numFrom = prevNum!! + 1, + numTo = it.number - 1 + ) + add(emptyLesson) + } + + if (it.isStudentPlan) { + val normalLesson = TimetableItem.Normal( + lesson = it, + showGroupsInPlan = prefRepository.showGroupsInPlan, + timeLeft = filteredItems.getTimeLeftForLesson(it, i), + onClick = ::onTimetableItemSelected + ) + add(normalLesson) + } else { + val smallLesson = TimetableItem.Small( + lesson = it, + onClick = ::onTimetableItemSelected + ) + add(smallLesson) + } + + prevNum = it.number + } } } @@ -242,7 +311,7 @@ class TimetablePresenter @Inject constructor( private fun reloadView(date: LocalDate) { currentDate = date - Timber.i("Reload timetable view with the date ${currentDate.toFormattedString()}") + Timber.i("Reload timetable view with the date ${currentDate?.toFormattedString()}") view?.apply { showProgress(true) enableSwipe(false) @@ -255,10 +324,13 @@ class TimetablePresenter @Inject constructor( } private fun reloadNavigation() { + val currentDate = currentDate ?: return + view?.apply { showPreButton(!currentDate.minusDays(1).isHolidays) showNextButton(!currentDate.plusDays(1).isHolidays) updateNavigationDay(currentDate.toFormattedString("EEEE, dd.MM").capitalise()) + showNavigation(true) } } 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 8cfb26204..40190d51f 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 @@ -36,6 +36,8 @@ interface TimetableView : BaseView { fun showContent(show: Boolean) + fun showNavigation(show: Boolean) + fun showPreButton(show: Boolean) fun showNextButton(show: Boolean) 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 043fa1f7d..faa833c20 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,8 +2,8 @@ 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.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.TimetableAdditional @@ -13,11 +13,7 @@ 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.dpToPx -import io.github.wulkanowy.utils.firstSchoolDayInSchoolYear -import io.github.wulkanowy.utils.getThemeAttrColor -import io.github.wulkanowy.utils.lastSchoolDayInSchoolYear -import io.github.wulkanowy.utils.openMaterialDatePicker +import io.github.wulkanowy.utils.* import java.time.LocalDate import javax.inject.Inject @@ -73,7 +69,7 @@ class AdditionalLessonsFragment : openAddAdditionalLessonButton.setOnClickListener { presenter.onAdditionalLessonAddButtonClicked() } - additionalLessonsNavContainer.elevation = requireContext().dpToPx(8f) + additionalLessonsNavContainer.elevation = requireContext().dpToPx(3f) } } @@ -154,7 +150,7 @@ class AdditionalLessonsFragment : } override fun showDeleteLessonDialog(timetableAdditional: TimetableAdditional) { - AlertDialog.Builder(requireContext()) + MaterialAlertDialogBuilder(requireContext()) .setTitle(getString(R.string.additional_lessons_delete_title)) .setItems( arrayOf( diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/add/AdditionalLessonAddDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/add/AdditionalLessonAddDialog.kt index f82d64830..134719979 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/add/AdditionalLessonAddDialog.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/add/AdditionalLessonAddDialog.kt @@ -1,10 +1,10 @@ package io.github.wulkanowy.ui.modules.timetable.additional.add +import android.app.Dialog 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.dialog.MaterialAlertDialogBuilder import com.google.android.material.timepicker.MaterialTimePicker import com.google.android.material.timepicker.TimeFormat import dagger.hilt.android.AndroidEntryPoint @@ -29,16 +29,14 @@ class AdditionalLessonAddDialog : BaseDialogFragment 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 onCreateDialog(savedInstanceState: Bundle?): Dialog { + return MaterialAlertDialogBuilder(requireContext(), theme) + .setView( + DialogAdditionalAddBinding.inflate(layoutInflater).apply { binding = this }.root + ) + .create() + } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonDialog.kt index 7d32278f0..d937d4dd0 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonDialog.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonDialog.kt @@ -1,17 +1,18 @@ package io.github.wulkanowy.ui.modules.timetable.completed +import android.app.Dialog import android.os.Bundle -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.DialogFragment +import androidx.core.os.bundleOf +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.data.db.entities.CompletedLesson import io.github.wulkanowy.databinding.DialogLessonCompletedBinding -import io.github.wulkanowy.utils.lifecycleAwareVariable +import io.github.wulkanowy.ui.base.BaseDialogFragment +import io.github.wulkanowy.utils.serializable -class CompletedLessonDialog : DialogFragment() { - - private var binding: DialogLessonCompletedBinding by lifecycleAwareVariable() +@AndroidEntryPoint +class CompletedLessonDialog : BaseDialogFragment() { private lateinit var completedLesson: CompletedLesson @@ -19,24 +20,23 @@ class CompletedLessonDialog : DialogFragment() { private const val ARGUMENT_KEY = "Item" - fun newInstance(exam: CompletedLesson) = CompletedLessonDialog().apply { - arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, exam) } + fun newInstance(lesson: CompletedLesson) = CompletedLessonDialog().apply { + arguments = bundleOf(ARGUMENT_KEY to lesson) } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setStyle(STYLE_NO_TITLE, 0) - arguments?.run { - completedLesson = getSerializable(ARGUMENT_KEY) as CompletedLesson - } + completedLesson = requireArguments().serializable(ARGUMENT_KEY) } - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ) = DialogLessonCompletedBinding.inflate(inflater).apply { binding = this }.root + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return MaterialAlertDialogBuilder(requireContext(), theme) + .setView( + DialogLessonCompletedBinding.inflate(layoutInflater).apply { binding = this }.root + ) + .create() + } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) 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 34a69e6ab..77a7bbd5a 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 @@ -2,9 +2,7 @@ package io.github.wulkanowy.ui.modules.timetable.completed import android.os.Bundle import android.view.View -import android.view.View.GONE -import android.view.View.INVISIBLE -import android.view.View.VISIBLE +import android.view.View.* import androidx.recyclerview.widget.LinearLayoutManager import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R @@ -14,12 +12,7 @@ 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.dpToPx -import io.github.wulkanowy.utils.firstSchoolDayInSchoolYear -import io.github.wulkanowy.utils.getCompatDrawable -import io.github.wulkanowy.utils.getThemeAttrColor -import io.github.wulkanowy.utils.lastSchoolDayInSchoolYear -import io.github.wulkanowy.utils.openMaterialDatePicker +import io.github.wulkanowy.utils.* import java.time.LocalDate import javax.inject.Inject @@ -73,7 +66,7 @@ class CompletedLessonsFragment : completedLessonsNavDate.setOnClickListener { presenter.onPickDate() } completedLessonsNextButton.setOnClickListener { presenter.onNextDay() } - completedLessonsNavContainer.elevation = requireContext().dpToPx(8f) + completedLessonsNavContainer.elevation = requireContext().dpToPx(3f) } } 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 6ef6cfc98..672dbe720 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 @@ -2,14 +2,11 @@ package io.github.wulkanowy.ui.modules.timetablewidget import android.appwidget.AppWidgetManager.* import android.content.Intent -import android.os.Build import android.os.Bundle import android.widget.Toast import android.widget.Toast.LENGTH_LONG -import androidx.appcompat.app.AlertDialog 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.ActivityWidgetConfigureBinding import io.github.wulkanowy.ui.base.BaseActivity @@ -34,8 +31,6 @@ class TimetableWidgetConfigureActivity : @Inject lateinit var appInfo: AppInfo - private var dialog: AlertDialog? = null - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setResult(RESULT_CANCELED) @@ -61,23 +56,6 @@ class TimetableWidgetConfigureActivity : configureAdapter.onClickListener = presenter::onItemSelect } - override fun showThemeDialog() { - var items = arrayOf( - getString(R.string.widget_timetable_theme_light), - getString(R.string.widget_timetable_theme_dark) - ) - - if (appInfo.systemVersion >= Build.VERSION_CODES.Q) items += getString(R.string.widget_timetable_theme_system) - - dialog = AlertDialog.Builder(this, R.style.WulkanowyTheme_WidgetAccountSwitcher) - .setTitle(R.string.widget_timetable_theme_title) - .setOnDismissListener { presenter.onDismissThemeView() } - .setSingleChoiceItems(items, -1) { _, which -> - presenter.onThemeSelect(which) - } - .show() - } - override fun updateData(data: List, selectedStudentId: Long) { with(configureAdapter) { selectedId = selectedStudentId @@ -110,9 +88,4 @@ class TimetableWidgetConfigureActivity : override fun openLoginView() { startActivity(LoginActivity.getStartIntent(this)) } - - override fun onDestroy() { - super.onDestroy() - dialog?.dismiss() - } } 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 dc2a7c6c7..87e89336c 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 @@ -8,7 +8,6 @@ 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 kotlinx.coroutines.flow.onEach import timber.log.Timber import javax.inject.Inject @@ -39,22 +38,9 @@ class TimetableWidgetConfigurePresenter @Inject constructor( fun onItemSelect(student: Student) { selectedStudent = student - - if (isFromProvider) registerStudent(selectedStudent) - else view?.showThemeDialog() - } - - fun onThemeSelect(index: Int) { - appWidgetId?.let { - sharedPref.putLong(getThemeWidgetKey(it), index.toLong()) - } registerStudent(selectedStudent) } - fun onDismissThemeView() { - view?.finishView() - } - private fun loadData() { resourceFlow { studentRepository.getSavedStudents(false) }.onEach { when (it) { @@ -65,10 +51,7 @@ class TimetableWidgetConfigurePresenter @Inject constructor( } ?: -1 when { it.data.isEmpty() -> view?.openLoginView() - it.data.size == 1 && !isFromProvider -> { - selectedStudent = it.data.single().student - view?.showThemeDialog() - } + it.data.size == 1 && !isFromProvider -> onItemSelect(it.data.single().student) else -> view?.updateData(it.data, selectedStudentId) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetConfigureView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetConfigureView.kt index accdc28dc..7740b9bbe 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetConfigureView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetConfigureView.kt @@ -11,8 +11,6 @@ interface TimetableWidgetConfigureView : BaseView { fun updateTimetableWidget(widgetId: Int) - fun showThemeDialog() - fun setSuccessResult(widgetId: Int) fun finishView() 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 664086bca..4e0578e2b 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 @@ -1,51 +1,52 @@ package io.github.wulkanowy.ui.modules.timetablewidget -import android.annotation.SuppressLint import android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_ID import android.content.Context import android.content.Intent +import android.content.res.Configuration import android.graphics.Paint.ANTI_ALIAS_FLAG import android.graphics.Paint.STRIKE_THRU_TEXT_FLAG import android.view.View.GONE import android.view.View.VISIBLE -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.Semester +import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Timetable -import io.github.wulkanowy.data.enums.TimetableMode +import io.github.wulkanowy.data.enums.TimetableGapsMode.BETWEEN_AND_BEFORE_LESSONS +import io.github.wulkanowy.data.enums.TimetableGapsMode.NO_GAPS import io.github.wulkanowy.data.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.getPlural import io.github.wulkanowy.utils.toFormattedString import kotlinx.coroutines.runBlocking import timber.log.Timber +import java.time.Instant import java.time.LocalDate class TimetableWidgetFactory( private val timetableRepository: TimetableRepository, private val studentRepository: StudentRepository, private val semesterRepository: SemesterRepository, - private val prefRepository: PreferencesRepository, private val sharedPref: SharedPrefProvider, + private val prefRepository: PreferencesRepository, private val context: Context, private val intent: Intent? ) : RemoteViewsService.RemoteViewsFactory { - private var lessons = emptyList() + private var items = emptyList() - private var savedCurrentTheme: Long? = null - - private var primaryColor: Int? = null + private var timetableCanceledColor: Int? = null private var textColor: Int? = null @@ -55,9 +56,9 @@ class TimetableWidgetFactory( override fun hasStableIds() = true - override fun getCount() = lessons.size + override fun getCount() = items.size - override fun getViewTypeCount() = 2 + override fun getViewTypeCount() = 3 override fun getItemId(position: Int) = position.toLong() @@ -70,195 +71,233 @@ class TimetableWidgetFactory( val date = LocalDate.ofEpochDay(sharedPref.getLong(getDateWidgetKey(appWidgetId), 0)) val studentId = sharedPref.getLong(getStudentWidgetKey(appWidgetId), 0) - 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 - ) - } - } - } - - private fun updateTheme(appWidgetId: Int) { - savedCurrentTheme = sharedPref.getLong(getCurrentThemeWidgetKey(appWidgetId), 0) - - if (savedCurrentTheme == 0L) { - primaryColor = R.color.colorPrimary - textColor = android.R.color.black - timetableChangeColor = R.color.timetable_change_dark - } else { - primaryColor = R.color.colorPrimaryLight - textColor = android.R.color.white - timetableChangeColor = R.color.timetable_change_light - } - } - - private fun getItemLayout(lesson: Timetable): Int { - return when { - prefRepository.showWholeClassPlan == TimetableMode.SMALL_OTHER_GROUP && !lesson.isStudentPlan -> { - if (savedCurrentTheme == 0L) R.layout.item_widget_timetable_small - else R.layout.item_widget_timetable_small_dark - } - savedCurrentTheme == 1L -> R.layout.item_widget_timetable_dark - else -> R.layout.item_widget_timetable - } - } - - private fun getLessons(date: LocalDate, studentId: Long) = try { - runBlocking { - if (!studentRepository.isStudentSaved()) return@runBlocking emptyList() - - val students = studentRepository.getSavedStudents() - val student = students.singleOrNull { it.student.id == studentId }?.student - ?: return@runBlocking emptyList() - - val semester = semesterRepository.getCurrentSemester(student) - timetableRepository.getTimetable(student, semester, date, date, false) - .toFirstResult().dataOrNull?.lessons.orEmpty() - .sortedWith(compareBy({ it.number }, { !it.isStudentPlan })) - .filter { - if (prefRepository.showWholeClassPlan == TimetableMode.ONLY_CURRENT_GROUP) { - it.isStudentPlan - } else true + runCatching { + runBlocking { + val student = getStudent(studentId) ?: return@runBlocking + val semester = semesterRepository.getCurrentSemester(student) + items = createItems( + lessons = getLessons(student, semester, date), + lastSync = timetableRepository.getLastRefreshTimestamp(semester, date, date) + ) + if (date == LocalDate.now()) { + updateTodayLastLessonEnd(appWidgetId) + } } + }.onFailure { + Timber.e(it, "An error has occurred in timetable widget factory") + } } - } catch (e: Exception) { - Timber.e(e, "An error has occurred in timetable widget factory") - emptyList() } - @SuppressLint("DefaultLocale") - override fun getViewAt(position: Int): RemoteViews? { - if (position == INVALID_POSITION || lessons.getOrNull(position) == null) return null + private suspend fun getStudent(studentId: Long): Student? { + val students = studentRepository.getSavedStudents() + return students.singleOrNull { it.student.id == studentId }?.student + } - val lesson = lessons[position] - return RemoteViews(context.packageName, getItemLayout(lesson)).apply { - setTextViewText(R.id.timetableWidgetItemSubject, lesson.subject) - setTextViewText(R.id.timetableWidgetItemNumber, lesson.number.toString()) - setTextViewText( - R.id.timetableWidgetItemTimeStart, - lesson.start.toFormattedString("HH:mm") - ) - setTextViewText( - R.id.timetableWidgetItemTimeFinish, - lesson.end.toFormattedString("HH:mm") - ) + private suspend fun getLessons( + student: Student, semester: Semester, date: LocalDate + ): List { + val timetable = timetableRepository.getTimetable(student, semester, date, date, false) + val lessons = timetable.toFirstResult().dataOrNull?.lessons.orEmpty() + return lessons.sortedBy { it.number } + } - updateDescription(this, lesson) - - if (lesson.canceled) { - updateStylesCanceled(this) - } else { - updateStylesNotCanceled(this, lesson) + private fun createItems( + lessons: List, + lastSync: Instant?, + ): List { + var prevNum = when (prefRepository.showTimetableGaps) { + BETWEEN_AND_BEFORE_LESSONS -> 0 + else -> null + } + return buildList { + lessons.forEach { + if (prefRepository.showTimetableGaps != NO_GAPS && prevNum != null && it.number > prevNum!! + 1) { + val emptyItem = TimetableWidgetItem.Empty( + numFrom = prevNum!! + 1, + numTo = it.number - 1 + ) + add(emptyItem) + } + add(TimetableWidgetItem.Normal(it)) + prevNum = it.number } + add(TimetableWidgetItem.Synchronized(lastSync ?: Instant.MIN)) + } + } + private fun updateTodayLastLessonEnd(appWidgetId: Int) { + val todayLastLessonEnd = items.filterIsInstance() + .maxOfOrNull { it.lesson.end } ?: return + val key = getTodayLastLessonEndDateTimeWidgetKey(appWidgetId) + sharedPref.putLong(key, todayLastLessonEnd.epochSecond, true) + } + + companion object { + const val TIME_FORMAT_STYLE = "HH:mm" + } + + override fun getViewAt(position: Int): RemoteViews? { + return when (val item = items.getOrNull(position) ?: return null) { + is TimetableWidgetItem.Normal -> getNormalItemRemoteView(item) + is TimetableWidgetItem.Empty -> getEmptyItemRemoteView(item) + is TimetableWidgetItem.Synchronized -> getSynchronizedItemRemoteView(item) + } + } + + private fun getNormalItemRemoteView(item: TimetableWidgetItem.Normal): RemoteViews { + val lesson = item.lesson + + val lessonStartTime = lesson.start.toFormattedString(TIME_FORMAT_STYLE) + val lessonEndTime = lesson.end.toFormattedString(TIME_FORMAT_STYLE) + + val remoteViews = RemoteViews(context.packageName, R.layout.item_widget_timetable).apply { + setTextViewText(R.id.timetableWidgetItemNumber, lesson.number.toString()) + setTextViewText(R.id.timetableWidgetItemTimeStart, lessonStartTime) + setTextViewText(R.id.timetableWidgetItemTimeFinish, lessonEndTime) + setTextViewText(R.id.timetableWidgetItemSubject, lesson.subject) + + setTextViewText(R.id.timetableWidgetItemTeacher, lesson.teacher) + setTextViewText(R.id.timetableWidgetItemDescription, lesson.info) setOnClickFillInIntent(R.id.timetableWidgetItemContainer, Intent()) } + updateTheme() + clearLessonStyles(remoteViews) + if (lesson.room.isBlank()) { + remoteViews.setViewVisibility(R.id.timetableWidgetItemRoom, GONE) + } else { + remoteViews.setTextViewText(R.id.timetableWidgetItemRoom, lesson.room) + } + when { + lesson.canceled -> applyCancelledLessonStyles(remoteViews) + lesson.changes or lesson.info.isNotBlank() -> applyChangedLessonStyles( + remoteViews = remoteViews, + lesson = lesson, + ) + } + return remoteViews } - private fun updateDescription(remoteViews: RemoteViews, lesson: Timetable) { - with(remoteViews) { - if (lesson.info.isNotBlank() && !lesson.changes) { - setTextViewText(R.id.timetableWidgetItemDescription, lesson.info) - setViewVisibility(R.id.timetableWidgetItemDescription, VISIBLE) - setViewVisibility(R.id.timetableWidgetItemRoom, GONE) - setViewVisibility(R.id.timetableWidgetItemTeacher, GONE) - } else { - setViewVisibility(R.id.timetableWidgetItemDescription, GONE) - setViewVisibility(R.id.timetableWidgetItemRoom, VISIBLE) - setViewVisibility(R.id.timetableWidgetItemTeacher, VISIBLE) + private fun getEmptyItemRemoteView(item: TimetableWidgetItem.Empty): RemoteViews { + return RemoteViews( + context.packageName, + R.layout.item_widget_timetable_empty + ).apply { + setTextViewText( + R.id.timetableWidgetEmptyItemNumber, + when (item.numFrom) { + item.numTo -> item.numFrom.toString() + else -> "${item.numFrom}-${item.numTo}" + } + ) + setTextViewText( + R.id.timetableWidgetEmptyItemText, + context.getPlural( + R.plurals.timetable_no_lesson, + item.numTo - item.numFrom + 1 + ) + ) + setOnClickFillInIntent(R.id.timetableWidgetEmptyItemContainer, Intent()) + } + } + + private fun getSynchronizedItemRemoteView(item: TimetableWidgetItem.Synchronized): RemoteViews { + return RemoteViews( + context.packageName, + R.layout.item_widget_timetable_footer + ).apply { + setTextViewText( + R.id.timetableWidgetSynchronizationTime, + getSynchronizationInfoText(item.timestamp) + ) + } + } + + private fun updateTheme() { + when (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) { + Configuration.UI_MODE_NIGHT_YES -> { + textColor = android.R.color.white + timetableChangeColor = R.color.timetable_change_dark + timetableCanceledColor = R.color.timetable_canceled_dark + } + + else -> { + textColor = android.R.color.black + timetableChangeColor = R.color.timetable_change_light + timetableCanceledColor = R.color.timetable_canceled_light } } } - private fun updateStylesCanceled(remoteViews: RemoteViews) { - with(remoteViews) { - setInt( - R.id.timetableWidgetItemSubject, "setPaintFlags", - STRIKE_THRU_TEXT_FLAG or ANTI_ALIAS_FLAG - ) - setTextColor(R.id.timetableWidgetItemNumber, context.getCompatColor(primaryColor!!)) - setTextColor(R.id.timetableWidgetItemSubject, context.getCompatColor(primaryColor!!)) - setTextColor( - R.id.timetableWidgetItemDescription, - context.getCompatColor(primaryColor!!) - ) - } - } + private fun clearLessonStyles(remoteViews: RemoteViews) { + val defaultTextColor = context.getCompatColor(textColor ?: 0) - private fun updateStylesNotCanceled(remoteViews: RemoteViews, lesson: Timetable) { - with(remoteViews) { + remoteViews.apply { setInt(R.id.timetableWidgetItemSubject, "setPaintFlags", ANTI_ALIAS_FLAG) - setTextColor(R.id.timetableWidgetItemSubject, context.getCompatColor(textColor!!)) - setTextColor( - R.id.timetableWidgetItemDescription, - context.getCompatColor(timetableChangeColor!!) - ) - - updateNotCanceledLessonNumberColor(this, lesson) - updateNotCanceledSubjectColor(this, lesson) - - val teacherChange = lesson.teacherOld.isNotBlank() - updateNotCanceledRoom(this, lesson, teacherChange) - updateNotCanceledTeacher(this, lesson, teacherChange) + setViewVisibility(R.id.timetableWidgetItemRoom, VISIBLE) + setViewVisibility(R.id.timetableWidgetItemTeacher, VISIBLE) + setViewVisibility(R.id.timetableWidgetItemIcon, GONE) + setViewVisibility(R.id.timetableWidgetItemDescription, GONE) + setTextColor(R.id.timetableWidgetItemNumber, defaultTextColor) + setTextColor(R.id.timetableWidgetItemSubject, defaultTextColor) + setTextColor(R.id.timetableWidgetItemRoom, defaultTextColor) + setTextColor(R.id.timetableWidgetItemTeacher, defaultTextColor) + setTextColor(R.id.timetableWidgetItemDescription, defaultTextColor) } } - private fun updateNotCanceledLessonNumberColor(remoteViews: RemoteViews, lesson: Timetable) { - remoteViews.setTextColor( - R.id.timetableWidgetItemNumber, context.getCompatColor( - if (lesson.changes || (lesson.info.isNotBlank() && !lesson.canceled)) timetableChangeColor!! - else textColor!! - ) - ) - } + private fun applyCancelledLessonStyles(remoteViews: RemoteViews) { + val cancelledThemeColor = context.getCompatColor(timetableCanceledColor ?: 0) + val strikeThroughPaintFlags = STRIKE_THRU_TEXT_FLAG or ANTI_ALIAS_FLAG - private fun updateNotCanceledSubjectColor(remoteViews: RemoteViews, lesson: Timetable) { - remoteViews.setTextColor( - R.id.timetableWidgetItemSubject, context.getCompatColor( - if (lesson.subjectOld.isNotBlank() && lesson.subject != lesson.subjectOld) timetableChangeColor!! - else textColor!! - ) - ) - } - - private fun updateNotCanceledRoom( - remoteViews: RemoteViews, - lesson: Timetable, - teacherChange: Boolean - ) { - with(remoteViews) { - if (lesson.room.isNotBlank()) { - setTextViewText( - R.id.timetableWidgetItemRoom, - if (teacherChange) lesson.room - else "${context.getString(R.string.timetable_room)} ${lesson.room}" - ) - - setTextColor( - R.id.timetableWidgetItemRoom, context.getCompatColor( - if (lesson.roomOld.isNotBlank() && lesson.room != lesson.roomOld) timetableChangeColor!! - else textColor!! - ) - ) - } else setTextViewText(R.id.timetableWidgetItemRoom, "") + remoteViews.apply { + setInt(R.id.timetableWidgetItemSubject, "setPaintFlags", strikeThroughPaintFlags) + setTextColor(R.id.timetableWidgetItemNumber, cancelledThemeColor) + setTextColor(R.id.timetableWidgetItemSubject, cancelledThemeColor) + setTextColor(R.id.timetableWidgetItemDescription, cancelledThemeColor) + setViewVisibility(R.id.timetableWidgetItemDescription, VISIBLE) + setViewVisibility(R.id.timetableWidgetItemRoom, GONE) + setViewVisibility(R.id.timetableWidgetItemTeacher, GONE) } } - private fun updateNotCanceledTeacher( - remoteViews: RemoteViews, - lesson: Timetable, - teacherChange: Boolean - ) { - remoteViews.setTextViewText( - R.id.timetableWidgetItemTeacher, - if (teacherChange) lesson.teacher - else "" - ) + private fun applyChangedLessonStyles(remoteViews: RemoteViews, lesson: Timetable) { + val changesTextColor = context.getCompatColor(timetableChangeColor ?: 0) + + remoteViews.apply { + setTextColor(R.id.timetableWidgetItemNumber, changesTextColor) + setTextColor(R.id.timetableWidgetItemDescription, changesTextColor) + setViewVisibility(R.id.timetableWidgetItemIcon, VISIBLE) + setImageViewResource(R.id.timetableWidgetItemIcon, R.drawable.ic_timetable_widget_swap) + } + + if (lesson.subject != lesson.subjectOld) { + remoteViews.setTextColor(R.id.timetableWidgetItemSubject, changesTextColor) + } + + if (lesson.room != lesson.roomOld) { + remoteViews.setTextColor(R.id.timetableWidgetItemRoom, changesTextColor) + } + + if (lesson.teacher != lesson.teacherOld) { + remoteViews.setTextColor(R.id.timetableWidgetItemTeacher, changesTextColor) + } + + if (lesson.info.isNotBlank() && !lesson.changes) { + remoteViews.setViewVisibility(R.id.timetableWidgetItemDescription, VISIBLE) + remoteViews.setViewVisibility(R.id.timetableWidgetItemRoom, GONE) + remoteViews.setViewVisibility(R.id.timetableWidgetItemTeacher, GONE) + } } + + private fun getSynchronizationInfoText(synchronizationInstant: Instant) = + synchronizationInstant.run { + val synchronizationTime = toFormattedString(TIME_FORMAT_STYLE) + val synchronizationDate = toFormattedString() + context.getString( + R.string.widget_timetable_last_synchronization, + synchronizationDate, + synchronizationTime, + ) + } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetItem.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetItem.kt new file mode 100644 index 000000000..166b1a8fb --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetItem.kt @@ -0,0 +1,26 @@ +package io.github.wulkanowy.ui.modules.timetablewidget + +import io.github.wulkanowy.data.db.entities.Timetable +import java.time.Instant + +sealed class TimetableWidgetItem(val type: TimetableWidgetItemType) { + + data class Normal( + val lesson: Timetable, + ) : TimetableWidgetItem(TimetableWidgetItemType.NORMAL) + + data class Empty( + val numFrom: Int, + val numTo: Int + ) : TimetableWidgetItem(TimetableWidgetItemType.EMPTY) + + data class Synchronized( + val timestamp: Instant, + ) : TimetableWidgetItem(TimetableWidgetItemType.SYNCHRONIZED) +} + +enum class TimetableWidgetItemType { + NORMAL, + EMPTY, + SYNCHRONIZED, +} 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 3ba2ae946..cc48539a5 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 @@ -8,10 +8,11 @@ import android.content.Context import android.content.Intent import android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK import android.content.Intent.FLAG_ACTIVITY_NEW_TASK -import android.content.res.Configuration import android.graphics.Bitmap -import android.graphics.Canvas import android.widget.RemoteViews +import androidx.appcompat.content.res.AppCompatResources +import androidx.core.graphics.drawable.DrawableCompat +import androidx.core.graphics.drawable.toBitmap import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.data.db.SharedPrefProvider @@ -70,175 +71,177 @@ class TimetableWidgetProvider : BroadcastReceiver() { "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" - - fun getCurrentThemeWidgetKey(appWidgetId: Int) = - "timetable_widget_current_theme_$appWidgetId" } @OptIn(DelicateCoroutinesApi::class) override fun onReceive(context: Context, intent: Intent) { GlobalScope.launch { when (intent.action) { - ACTION_APPWIDGET_UPDATE -> onUpdate(context, intent) - ACTION_APPWIDGET_DELETED -> onDelete(intent) + ACTION_APPWIDGET_UPDATE -> onWidgetUpdate(context, intent) + ACTION_APPWIDGET_DELETED -> onWidgetDeleted(intent) } } } - private suspend fun onUpdate(context: Context, intent: Intent) { - if (intent.getStringExtra(EXTRA_BUTTON_TYPE) == null) { - val isFromConfigure = intent.getBooleanExtra(EXTRA_FROM_CONFIGURE, false) - val appWidgetIds = intent.getIntArrayExtra(EXTRA_APPWIDGET_IDS) ?: return + private suspend fun onWidgetUpdate(context: Context, intent: Intent) { + val pressedButton = intent.getPressedButton() - appWidgetIds.forEach { appWidgetId -> - val student = - getStudent(sharedPref.getLong(getStudentWidgetKey(appWidgetId), 0), appWidgetId) - val savedDataEpochDay = sharedPref.getLong(getDateWidgetKey(appWidgetId), 0) - - val dateToLoad = if (isFromConfigure && savedDataEpochDay != 0L) { - LocalDate.ofEpochDay(savedDataEpochDay) - } else { - getWidgetDefaultDateToLoad(appWidgetId) - } - - updateWidget(context, appWidgetId, dateToLoad, student) - } + if (pressedButton == null) { + val updatedWidgetIds = intent.getWidgetIds() ?: return + updatedWidgetIds.forEach { updateWidgetLayout(context, it) } } else { - val buttonType = intent.getStringExtra(EXTRA_BUTTON_TYPE) - val toggledWidgetId = intent.getIntExtra(EXTRA_TOGGLED_WIDGET_ID, 0) - val student = getStudent( - sharedPref.getLong(getStudentWidgetKey(toggledWidgetId), 0), - toggledWidgetId - ) - val savedDate = - LocalDate.ofEpochDay(sharedPref.getLong(getDateWidgetKey(toggledWidgetId), 0)) - val date = when (buttonType) { - BUTTON_RESET -> getWidgetDefaultDateToLoad(toggledWidgetId) - BUTTON_NEXT -> savedDate.nextSchoolDay - BUTTON_PREV -> savedDate.previousSchoolDay - else -> getWidgetDefaultDateToLoad(toggledWidgetId) - } - if (!buttonType.isNullOrBlank()) { - analytics.logEvent( - "changed_timetable_widget_day", - "button" to buttonType - ) - } - updateWidget(context, toggledWidgetId, date, student) + val widgetId = intent.getToggledWidgetId() ?: return + reportChangedDay(pressedButton) + updateSavedWidgetDate(widgetId, pressedButton) + updateWidgetLayout(context, widgetId) } } - private fun onDelete(intent: Intent) { - val appWidgetId = intent.getIntExtra(EXTRA_APPWIDGET_ID, 0) + private fun Intent.getPressedButton(): String? { + return getStringExtra(EXTRA_BUTTON_TYPE) + } - if (appWidgetId != 0) { - with(sharedPref) { - delete(getStudentWidgetKey(appWidgetId)) - delete(getDateWidgetKey(appWidgetId)) - delete(getThemeWidgetKey(appWidgetId)) - delete(getCurrentThemeWidgetKey(appWidgetId)) - } + private fun Intent.getWidgetIds(): IntArray? { + return getIntArrayExtra(EXTRA_APPWIDGET_IDS) + } + + private fun Intent.getToggledWidgetId(): Int? { + val toggledWidgetId = getIntExtra(EXTRA_TOGGLED_WIDGET_ID, INVALID_APPWIDGET_ID) + return toggledWidgetId.takeIf { it != INVALID_APPWIDGET_ID } + } + + private fun reportChangedDay(buttonType: String) { + if (buttonType.isNotBlank()) { + analytics.logEvent("changed_timetable_widget_day", "button" to buttonType) } } - private fun updateWidget( - context: Context, - appWidgetId: Int, - date: LocalDate, - student: Student? + private fun updateSavedWidgetDate(widgetId: Int, buttonType: String) { + val savedDate = getSavedWidgetDate(widgetId) + val newDate = savedDate?.let { getNewDate(it, widgetId, buttonType) } + ?: getWidgetDefaultDateToLoad(widgetId) + setWidgetDate(widgetId, newDate) + } + + private fun getSavedWidgetDate(widgetId: Int): LocalDate? { + val epochDay = sharedPref.getLong(getDateWidgetKey(widgetId), 0) + return if (epochDay == 0L) null else LocalDate.ofEpochDay(epochDay) + } + + private fun getNewDate( + currentDate: LocalDate, + widgetId: Int, + selectedButton: String + ): LocalDate { + return when (selectedButton) { + BUTTON_NEXT -> currentDate.nextSchoolDay + BUTTON_PREV -> currentDate.previousSchoolDay + else -> getWidgetDefaultDateToLoad(widgetId) + } + } + + private fun setWidgetDate(widgetId: Int, dateToSet: LocalDate) { + val widgetDateKey = getDateWidgetKey(widgetId) + sharedPref.putLong(widgetDateKey, dateToSet.toEpochDay(), true) + } + + private fun getWidgetDefaultDateToLoad(widgetId: Int): LocalDate { + val lastLessonEndDateTime = getLastLessonDateTime(widgetId) + + val todayDate = LocalDate.now() + val isLastLessonToday = lastLessonEndDateTime.toLocalDate() == todayDate + val isEndOfLessons = LocalDateTime.now() > lastLessonEndDateTime + + return if (isLastLessonToday && isEndOfLessons) { + todayDate.nextSchoolDay + } else { + todayDate.nextOrSameSchoolDay + } + } + + private fun getLastLessonDateTime(widgetId: Int): LocalDateTime { + val lastLessonTimestamp = sharedPref + .getLong(getTodayLastLessonEndDateTimeWidgetKey(widgetId), 0) + return LocalDateTime.ofEpochSecond(lastLessonTimestamp, 0, ZoneOffset.UTC) + } + + private suspend fun updateWidgetLayout( + context: Context, widgetId: Int ) { - val savedConfigureTheme = sharedPref.getLong(getThemeWidgetKey(appWidgetId), 0) - val isSystemDarkMode = - context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES - var currentTheme = 0L - var layoutId = R.layout.widget_timetable + val widgetRemoteViews = RemoteViews(context.packageName, R.layout.widget_timetable) - if (savedConfigureTheme == 1L || (savedConfigureTheme == 2L && isSystemDarkMode)) { - currentTheme = 1L - layoutId = R.layout.widget_timetable_dark - } + // Apply the click action intent + val appIntent = createPendingAppIntent(context) + widgetRemoteViews.setPendingIntentTemplate(R.id.timetableWidgetList, appIntent) - val nextNavIntent = createNavIntent(context, appWidgetId, appWidgetId, BUTTON_NEXT) - val prevNavIntent = createNavIntent(context, -appWidgetId, appWidgetId, BUTTON_PREV) + // Display saved date + val date = getSavedWidgetDate(widgetId) ?: getWidgetDefaultDateToLoad(widgetId) + val formattedDate = date.toFormattedString("EEE, dd.MM").capitalise() + widgetRemoteViews.setTextViewText(R.id.timetableWidgetDate, formattedDate) + + // Apply intents to the date switcher buttons + val nextNavIntent = createNavButtonIntent(context, widgetId, widgetId, BUTTON_NEXT) + val prevNavIntent = createNavButtonIntent(context, -widgetId, widgetId, BUTTON_PREV) val resetNavIntent = - createNavIntent(context, Int.MAX_VALUE - appWidgetId, appWidgetId, BUTTON_RESET) - val adapterIntent = Intent(context, TimetableWidgetService::class.java) - .apply { - putExtra(EXTRA_APPWIDGET_ID, appWidgetId) - //make Intent unique - action = appWidgetId.toString() - } - val accountIntent = PendingIntent.getActivity( - context, - -Int.MAX_VALUE + appWidgetId, - Intent(context, TimetableWidgetConfigureActivity::class.java).apply { - addFlags(FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_CLEAR_TASK) - putExtra(EXTRA_APPWIDGET_ID, appWidgetId) - putExtra(EXTRA_FROM_PROVIDER, true) - }, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE - - ) - val appIntent = PendingIntent.getActivity( - context, - TIMETABLE_PENDING_INTENT_ID, - SplashActivity.getStartIntent(context, Destination.Timetable()), - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE - ) - - val remoteView = RemoteViews(context.packageName, layoutId).apply { - setEmptyView(R.id.timetableWidgetList, R.id.timetableWidgetEmpty) - setTextViewText( - R.id.timetableWidgetDate, - date.toFormattedString("EEEE, dd.MM").capitalise() - ) - setTextViewText( - R.id.timetableWidgetName, - student?.nickOrName ?: context.getString(R.string.all_no_data) - ) - - student?.let { - setImageViewBitmap(R.id.timetableWidgetAccount, context.createAvatarBitmap(it)) - } - - setRemoteAdapter(R.id.timetableWidgetList, adapterIntent) + createNavButtonIntent(context, Int.MAX_VALUE - widgetId, widgetId, BUTTON_RESET) + widgetRemoteViews.run { setOnClickPendingIntent(R.id.timetableWidgetNext, nextNavIntent) setOnClickPendingIntent(R.id.timetableWidgetPrev, prevNavIntent) setOnClickPendingIntent(R.id.timetableWidgetDate, resetNavIntent) - setOnClickPendingIntent(R.id.timetableWidgetName, resetNavIntent) - setOnClickPendingIntent(R.id.timetableWidgetAccount, accountIntent) - setPendingIntentTemplate(R.id.timetableWidgetList, appIntent) } - with(sharedPref) { - putLong(getCurrentThemeWidgetKey(appWidgetId), currentTheme) - putLong(getDateWidgetKey(appWidgetId), date.toEpochDay(), true) + // Setup the lesson list adapter + val lessonListAdapterIntent = createLessonListAdapterIntent(context, widgetId) + // --- Ensure the selected date is stored in the shared preferences, + // --- on which the TimetableWidgetFactory relies + setWidgetDate(widgetId, date) + // --- + widgetRemoteViews.apply { + setEmptyView(R.id.timetableWidgetList, R.id.timetableWidgetEmpty) + setRemoteAdapter(R.id.timetableWidgetList, lessonListAdapterIntent) } + // Setup profile picture + getWidgetStudent(widgetId)?.let { student -> + setupAccountView(context, student, widgetRemoteViews, widgetId) + } + + // Apply updates with(appWidgetManager) { - updateAppWidget(appWidgetId, remoteView) - notifyAppWidgetViewDataChanged(appWidgetId, R.id.timetableWidgetList) - Timber.d("TimetableWidgetProvider updated") + partiallyUpdateAppWidget(widgetId, widgetRemoteViews) + notifyAppWidgetViewDataChanged(widgetId, R.id.timetableWidgetList) } + + Timber.d("TimetableWidgetProvider updated") } - private fun createNavIntent( - context: Context, - code: Int, - appWidgetId: Int, - buttonType: String + private fun createPendingAppIntent(context: Context) = PendingIntent.getActivity( + context, TIMETABLE_PENDING_INTENT_ID, + SplashActivity.getStartIntent(context, Destination.Timetable()), + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE + ) + + private fun createNavButtonIntent( + context: Context, code: Int, appWidgetId: Int, buttonType: String ) = PendingIntent.getBroadcast( - context, - code, - Intent(context, TimetableWidgetProvider::class.java).apply { + context, code, Intent(context, TimetableWidgetProvider::class.java).apply { action = ACTION_APPWIDGET_UPDATE putExtra(EXTRA_BUTTON_TYPE, buttonType) putExtra(EXTRA_TOGGLED_WIDGET_ID, appWidgetId) - }, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE + }, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE ) + private fun createLessonListAdapterIntent(context: Context, widgetId: Int) = + Intent(context, TimetableWidgetService::class.java).apply { + putExtra(EXTRA_APPWIDGET_ID, widgetId) + action = widgetId.toString() //make Intent unique + } + + private suspend fun getWidgetStudent(widgetId: Int): Student? { + val studentId = sharedPref.getLong(getStudentWidgetKey(widgetId), 0) + return getStudent(studentId, widgetId) + } + private suspend fun getStudent(studentId: Long, appWidgetId: Int) = try { val students = studentRepository.getSavedStudents(false) val student = students.singleOrNull { it.student.id == studentId }?.student @@ -249,6 +252,7 @@ class TimetableWidgetProvider : BroadcastReceiver() { sharedPref.putLong(getStudentWidgetKey(appWidgetId), it.id) } } + else -> null } } catch (e: Exception) { @@ -258,45 +262,64 @@ class TimetableWidgetProvider : BroadcastReceiver() { null } - private fun Context.createAvatarBitmap(student: Student): Bitmap { - val avatarColor = if (student.avatarColor == -2937041L) { - getCompatColor(R.color.colorPrimaryLight).toLong() - } else { - student.avatarColor + private fun setupAccountView( + context: Context, student: Student, remoteViews: RemoteViews, widgetId: Int + ) { + val accountInitials = getAccountInitials(student.nickOrName) + val accountPickerPendingIntent = createAccountPickerPendingIntent(context, widgetId) + + getAvatarBackgroundBitmap(context, student.avatarColor)?.let { + remoteViews.setImageViewBitmap(R.id.timetableWidgetAccountBackground, it) } - val avatarDrawable = createNameInitialsDrawable(student.nickOrName, avatarColor, 0.5f) - val avatarBitmap = - if (avatarDrawable.intrinsicWidth <= 0 || avatarDrawable.intrinsicHeight <= 0) { - Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888) - } else { - Bitmap.createBitmap( - avatarDrawable.intrinsicWidth, - avatarDrawable.intrinsicHeight, - Bitmap.Config.ARGB_8888 - ) - } - - val canvas = Canvas(avatarBitmap) - avatarDrawable.setBounds(0, 0, canvas.width, canvas.height) - avatarDrawable.draw(canvas) - return avatarBitmap + remoteViews.apply { + setTextViewText(R.id.timetableWidgetAccountInitials, accountInitials) + setOnClickPendingIntent(R.id.timetableWidgetAccount, accountPickerPendingIntent) + } } - private fun getWidgetDefaultDateToLoad(appWidgetId: Int): LocalDate { - val lastLessonEndTimestamp = - sharedPref.getLong(getTodayLastLessonEndDateTimeWidgetKey(appWidgetId), 0) - val lastLessonEndDateTime = - LocalDateTime.ofEpochSecond(lastLessonEndTimestamp, 0, ZoneOffset.UTC) + private fun getAccountInitials(name: String): String { + val firstLetters = name.split(" ").mapNotNull { it.firstOrNull() } + return firstLetters.joinToString(separator = "").uppercase() + } - val todayDate = LocalDate.now() - val isLastLessonEndDateNow = lastLessonEndDateTime.toLocalDate() == todayDate - val isLastLessonEndDateAfterNowTime = LocalDateTime.now() > lastLessonEndDateTime + private fun createAccountPickerPendingIntent(context: Context, widgetId: Int) = + PendingIntent.getActivity( + context, + -Int.MAX_VALUE + widgetId, + Intent(context, TimetableWidgetConfigureActivity::class.java).apply { + addFlags(FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_CLEAR_TASK) + putExtra(EXTRA_APPWIDGET_ID, widgetId) + putExtra(EXTRA_FROM_PROVIDER, true) + }, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE + ) - return if (isLastLessonEndDateNow && isLastLessonEndDateAfterNowTime) { - todayDate.nextSchoolDay - } else { - todayDate.nextOrSameSchoolDay + private fun getAvatarBackgroundBitmap(context: Context, avatarColor: Long): Bitmap? { + val avatarDrawableResource = R.drawable.background_timetable_widget_avatar + return AppCompatResources.getDrawable(context, avatarDrawableResource)?.let { drawable -> + val screenDensity = context.resources.displayMetrics.density + val avatarSize = (48 * screenDensity).toInt() + DrawableCompat.wrap(drawable).run { + DrawableCompat.setTint(this, avatarColor.toInt()) + toBitmap(avatarSize, avatarSize) + } + } + } + + private fun onWidgetDeleted(intent: Intent) { + val deletedWidgetId = intent.getWidgetId() + deleteWidgetPreferences(deletedWidgetId) + } + + private fun Intent.getWidgetId(): Int { + return getIntExtra(EXTRA_APPWIDGET_ID, INVALID_APPWIDGET_ID) + } + + private fun deleteWidgetPreferences(widgetId: Int) { + with(sharedPref) { + delete(getStudentWidgetKey(widgetId)) + delete(getDateWidgetKey(widgetId)) } } } diff --git a/app/src/main/java/io/github/wulkanowy/utils/AppInfo.kt b/app/src/main/java/io/github/wulkanowy/utils/AppInfo.kt index 962e5b208..e16db53c6 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/AppInfo.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/AppInfo.kt @@ -25,7 +25,8 @@ open class AppInfo @Inject constructor() { open val systemModel: String get() = Build.MODEL - open val messagesBaseUrl = BuildConfig.MESSAGES_BASE_URL + open val messagesBaseUrl: String = BuildConfig.MESSAGES_BASE_URL + open val schoolsBaseUrl: String = BuildConfig.SCHOOLS_BASE_URL @Suppress("DEPRECATION") open val systemLanguage: String diff --git a/app/src/main/java/io/github/wulkanowy/utils/BaseRemoteConfigHelper.kt b/app/src/main/java/io/github/wulkanowy/utils/BaseRemoteConfigHelper.kt new file mode 100644 index 000000000..002612a87 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/utils/BaseRemoteConfigHelper.kt @@ -0,0 +1,9 @@ +package io.github.wulkanowy.utils + +abstract class BaseRemoteConfigHelper { + + open fun initialize() = Unit + + open val userAgentTemplate: String + get() = RemoteConfigDefaults.USER_AGENT_TEMPLATE.value as String +} diff --git a/app/src/main/java/io/github/wulkanowy/utils/BundleExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/BundleExtension.kt new file mode 100644 index 000000000..d3c9f8006 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/utils/BundleExtension.kt @@ -0,0 +1,33 @@ +package io.github.wulkanowy.utils + +import android.content.Intent +import android.os.Build +import android.os.Bundle +import android.os.Parcelable +import java.io.Serializable + +inline fun Bundle.serializable(key: String): T = when { + Build.VERSION.SDK_INT >= 33 -> getSerializable(key, T::class.java)!! + else -> @Suppress("DEPRECATION") getSerializable(key) as T +} + +inline fun Bundle.nullableSerializable(key: String): T? = when { + Build.VERSION.SDK_INT >= 33 -> getSerializable(key, T::class.java) + else -> @Suppress("DEPRECATION") getSerializable(key) as T? +} + +@Suppress("UNCHECKED_CAST") +inline fun Bundle.parcelableArray(key: String): Array? = when { + Build.VERSION.SDK_INT >= 33 -> getParcelableArray(key, T::class.java) + else -> @Suppress("DEPRECATION") getParcelableArray(key) as Array? +} + +inline fun Intent.serializable(key: String): T = when { + Build.VERSION.SDK_INT >= 33 -> getSerializableExtra(key, T::class.java)!! + else -> @Suppress("DEPRECATION") getSerializableExtra(key) as T +} + +inline fun Intent.nullableSerializable(key: String): T? = when { + Build.VERSION.SDK_INT >= 33 -> getSerializableExtra(key, T::class.java) + else -> @Suppress("DEPRECATION") getSerializableExtra(key) as T? +} 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 dd91d36d4..77f3eb64a 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/ContextExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/ContextExtension.kt @@ -2,6 +2,7 @@ package io.github.wulkanowy.utils import android.annotation.SuppressLint import android.content.Context +import android.content.res.ColorStateList import android.graphics.* import android.text.TextPaint import android.util.DisplayMetrics.DENSITY_DEFAULT @@ -85,3 +86,7 @@ fun Context.createNameInitialsDrawable( return RoundedBitmapDrawableFactory.create(this.resources, bitmap) .apply { isCircular = true } } + +fun Context.getAttrColorStateList(@AttrRes color: Int): ColorStateList { + return ColorStateList.valueOf(getThemeAttrColor(color)) +} diff --git a/app/src/main/java/io/github/wulkanowy/utils/ExceptionExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/ExceptionExtension.kt index 43cecd400..18fc10bba 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/ExceptionExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/ExceptionExtension.kt @@ -3,6 +3,7 @@ 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.CloudflareVerificationException import io.github.wulkanowy.sdk.scrapper.exception.FeatureDisabledException import io.github.wulkanowy.sdk.scrapper.exception.ScrapperException import io.github.wulkanowy.sdk.scrapper.exception.ServiceUnavailableException @@ -15,16 +16,17 @@ import java.net.ConnectException import java.net.SocketException import java.net.SocketTimeoutException import java.net.UnknownHostException +import java.security.cert.CertPathValidatorException 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 ConnectException, 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 @@ -33,6 +35,7 @@ fun Resources.getErrorString(error: Throwable): String = when (error) { is FeatureNotAvailableException -> R.string.error_feature_not_available is VulcanException -> R.string.error_unknown_uonet is ScrapperException -> R.string.error_unknown_app + is CloudflareVerificationException -> R.string.error_cloudflare_captcha is SSLHandshakeException -> when { error.isCausedByCertificateNotValidNow() -> R.string.error_invalid_device_datetime else -> R.string.error_timeout @@ -42,10 +45,10 @@ fun Resources.getErrorString(error: Throwable): String = when (error) { fun Throwable.isShouldBeReported(): Boolean = when (this) { is UnknownHostException, + is ConnectException, is SocketException, is SocketTimeoutException, is InterruptedIOException, - is ConnectException, is StreamResetException, is ServiceUnavailableException, is FeatureDisabledException, @@ -70,5 +73,6 @@ private fun Throwable?.isCausedByCertificateNotValidNow(): Boolean { private fun Throwable?.isCertificateNotValidNow(): Boolean { val isNotYetValid = this is CertificateNotYetValidException val isExpired = this is CertificateExpiredException - return isNotYetValid || isExpired + val isInvalidPath = this is CertPathValidatorException + return isNotYetValid || isExpired || isInvalidPath } diff --git a/app/src/main/java/io/github/wulkanowy/utils/GradeExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/GradeExtension.kt index ff65d6376..61924d4e9 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/GradeExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/GradeExtension.kt @@ -4,6 +4,7 @@ import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Grade import io.github.wulkanowy.data.db.entities.GradeSummary import io.github.wulkanowy.data.enums.GradeColorTheme +import io.github.wulkanowy.sdk.scrapper.grades.getGradeValueWithModifier import io.github.wulkanowy.sdk.scrapper.grades.isGradeValid fun List.calcAverage(isOptionalArithmeticAverage: Boolean): Double { @@ -20,20 +21,15 @@ fun List.calcAverage(isOptionalArithmeticAverage: Boolean): Double { } fun List.calcFinalAverage(plusModifier: Double, minusModifier: Double) = asSequence() - .mapNotNull { - if (it.finalGrade.matches("[0-6][+-]?".toRegex())) { - when { - it.finalGrade.endsWith('+') -> { - it.finalGrade.removeSuffix("+").toDouble() + plusModifier - } - it.finalGrade.endsWith('-') -> { - it.finalGrade.removeSuffix("-").toDouble() - minusModifier - } - else -> { - it.finalGrade.toDouble() - } - } - } else null + .mapNotNull { summary -> + val (gradeValue, gradeModifier) = getGradeValueWithModifier(summary.finalGrade) + if (gradeValue == null || gradeModifier == null) return@mapNotNull null + + when { + gradeModifier > 0 -> gradeValue + plusModifier + gradeModifier < 0 -> gradeValue - minusModifier + else -> gradeValue + 0.0 + } } .average() .let { if (it.isNaN()) 0.0 else it } diff --git a/app/src/main/java/io/github/wulkanowy/utils/IntentUtils.kt b/app/src/main/java/io/github/wulkanowy/utils/IntentUtils.kt index 1ef03f2e6..62b85af4d 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/IntentUtils.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/IntentUtils.kt @@ -1,11 +1,15 @@ package io.github.wulkanowy.utils +import android.app.Activity import android.content.ActivityNotFoundException import android.content.Context import android.content.Intent import android.net.Uri +import android.os.Build import android.provider.CalendarContract +import android.provider.Settings import io.github.wulkanowy.BuildConfig +import timber.log.Timber import java.time.LocalDateTime import java.time.ZoneId @@ -86,6 +90,23 @@ fun Context.openDialer(phone: String) { } } +fun Activity.openNotificationSettings() { + val intent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply { + putExtra("android.provider.extra.APP_PACKAGE", packageName) + } + } else { + Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { + data = Uri.fromParts("package", packageName, null) + } + } + try { + startActivity(intent) + } catch (e: Exception) { + Timber.e(e) + } +} + fun Context.shareText(text: String, subject: String?) { val sendIntent: Intent = Intent().apply { action = Intent.ACTION_SEND diff --git a/app/src/main/java/io/github/wulkanowy/utils/LifecycleAwareVariable.kt b/app/src/main/java/io/github/wulkanowy/utils/LifecycleAwareVariable.kt index 032e2d28a..76ce66dc5 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/LifecycleAwareVariable.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/LifecycleAwareVariable.kt @@ -3,6 +3,7 @@ package io.github.wulkanowy.utils import android.os.Handler import android.os.Looper import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.DialogFragment import androidx.fragment.app.Fragment import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner @@ -29,18 +30,18 @@ class LifecycleAwareVariable : ReadWriteProperty, DefaultL } } -class LifecycleAwareVariableActivity : ReadWriteProperty, +class LifecycleAwareVariableComponent : ReadWriteProperty, DefaultLifecycleObserver { private var _value: T? = null - override fun setValue(thisRef: AppCompatActivity, property: KProperty<*>, value: T) { + override fun setValue(thisRef: LifecycleOwner, property: KProperty<*>, value: T) { thisRef.lifecycle.removeObserver(this) _value = value thisRef.lifecycle.addObserver(this) } - override fun getValue(thisRef: AppCompatActivity, property: KProperty<*>) = _value + override fun getValue(thisRef: LifecycleOwner, property: KProperty<*>) = _value ?: throw IllegalStateException("Trying to call an lifecycle-aware value outside of the view lifecycle, or the value has not been initialized") override fun onDestroy(owner: LifecycleOwner) { @@ -53,4 +54,8 @@ class LifecycleAwareVariableActivity : ReadWriteProperty Fragment.lifecycleAwareVariable() = LifecycleAwareVariable() -fun lifecycleAwareVariable() = LifecycleAwareVariableActivity() +@Suppress("unused") +fun DialogFragment.lifecycleAwareVariable() = LifecycleAwareVariableComponent() + +@Suppress("unused") +fun AppCompatActivity.lifecycleAwareVariable() = LifecycleAwareVariableComponent() diff --git a/app/src/main/java/io/github/wulkanowy/utils/LoggerUtils.kt b/app/src/main/java/io/github/wulkanowy/utils/LoggerUtils.kt index 1e9f49a66..00ed2c110 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/LoggerUtils.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/LoggerUtils.kt @@ -7,7 +7,7 @@ import android.os.Bundle import android.view.View import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager -import fr.bipi.tressence.common.filters.Filter +import fr.bipi.treessence.common.filters.Filter import io.github.wulkanowy.sdk.exception.FeatureNotAvailableException import io.github.wulkanowy.sdk.scrapper.exception.FeatureDisabledException import timber.log.Timber 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 c69fec65c..721297513 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/RefreshUtils.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/RefreshUtils.kt @@ -4,6 +4,7 @@ import android.content.Context import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.data.db.SharedPrefProvider +import io.github.wulkanowy.data.db.entities.Mailbox import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.enums.MessageFolder @@ -25,8 +26,8 @@ fun getRefreshKey(name: String, student: Student): String { return "${name}_${student.userLoginId}" } -fun getRefreshKey(name: String, student: Student, folder: MessageFolder): String { - return "${name}_${student.id}_${folder.id}" +fun getRefreshKey(name: String, mailbox: Mailbox?, folder: MessageFolder): String { + return "${name}_${mailbox?.globalKey ?: "all"}_${folder.id}" } class AutoRefreshHelper @Inject constructor( @@ -48,4 +49,9 @@ class AutoRefreshHelper @Inject constructor( fun updateLastRefreshTimestamp(key: String) { sharedPref.putLong(key, Instant.now().toEpochMilli()) } + + fun getLastRefreshTimestamp(key: String): Instant { + val refreshTimestampMilli = sharedPref.getLong(key, 0) + return Instant.ofEpochMilli(refreshTimestampMilli) + } } diff --git a/app/src/main/java/io/github/wulkanowy/utils/RemoteConfigDefaults.kt b/app/src/main/java/io/github/wulkanowy/utils/RemoteConfigDefaults.kt new file mode 100644 index 000000000..6e8f7ae65 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/utils/RemoteConfigDefaults.kt @@ -0,0 +1,8 @@ +package io.github.wulkanowy.utils + +enum class RemoteConfigDefaults(val value: Any) { + USER_AGENT_TEMPLATE(""), + ; + + val key get() = name.lowercase() +} diff --git a/app/src/main/java/io/github/wulkanowy/utils/SdkExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/SdkExtension.kt index 63a30db8c..9b6ca7060 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/SdkExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/SdkExtension.kt @@ -1,5 +1,6 @@ package io.github.wulkanowy.utils +import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.sdk.Sdk import timber.log.Timber @@ -11,21 +12,31 @@ fun Sdk.init(student: Student): Sdk { schoolSymbol = student.schoolSymbol studentId = student.studentId classId = student.classId + emptyCookieJarInterceptor = true - if (Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API) { + if (Sdk.Mode.valueOf(student.loginMode) == Sdk.Mode.HEBE) { + mobileBaseUrl = student.mobileBaseUrl + } else { scrapperBaseUrl = student.scrapperBaseUrl + domainSuffix = student.scrapperDomainSuffix loginType = Sdk.ScrapperLoginType.valueOf(student.loginType) } - loginId = student.userLoginId mode = Sdk.Mode.valueOf(student.loginMode) mobileBaseUrl = student.mobileBaseUrl - certKey = student.certificateKey - privateKey = student.privateKey - - emptyCookieJarInterceptor = true + keyId = student.certificateKey + privatePem = student.privateKey Timber.d("Sdk in ${student.loginMode} mode reinitialized") return this } + +fun Sdk.switchSemester(semester: Semester): Sdk { + return switchDiary( + diaryId = semester.diaryId, + kindergartenDiaryId = semester.kindergartenDiaryId, + schoolYear = semester.schoolYear, + unitId = semester.unitId, + ) +} diff --git a/app/src/main/java/io/github/wulkanowy/utils/SemesterExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/SemesterExtension.kt index 380d6bf6e..e3b8a3b4c 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/SemesterExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/SemesterExtension.kt @@ -1,16 +1,27 @@ package io.github.wulkanowy.utils import io.github.wulkanowy.data.db.entities.Semester +import java.time.LocalDate import java.time.LocalDate.now +import java.time.Month -inline val Semester.isCurrent: Boolean - get() = now() in start..end +fun Semester.isCurrent(now: LocalDate = now()): Boolean { + val shiftedStart = if (start.month == Month.SEPTEMBER) { + start.minusDays(3) + } else start + + val shiftedEnd = if (end.month == Month.AUGUST || end.month == Month.SEPTEMBER) { + end.minusDays(3) + } else end + + return now in shiftedStart..shiftedEnd +} fun List.getCurrentOrLast(): Semester { if (isEmpty()) throw RuntimeException("Empty semester list") // when there is only one current semester - singleOrNull { it.isCurrent }?.let { return it } + singleOrNull { it.isCurrent() }?.let { return it } // when there is more than one current semester - find one with higher id singleOrNull { semester -> semester.semesterId == maxByOrNull { it.semesterId }?.semesterId }?.let { return it } diff --git a/app/src/main/java/io/github/wulkanowy/utils/StringExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/StringExtension.kt index bddd7df4c..8043e3659 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/StringExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/StringExtension.kt @@ -1,7 +1,15 @@ package io.github.wulkanowy.utils +import androidx.core.text.parseAsHtml +import org.apache.commons.text.StringEscapeUtils + inline fun String?.ifNullOrBlank(defaultValue: () -> String) = if (isNullOrBlank()) defaultValue() else this fun String.capitalise() = - replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() } \ No newline at end of file + replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() } + +fun String.parseUonetHtml() = this + .let(StringEscapeUtils::unescapeHtml4) + .replace("\n", "
") + .parseAsHtml() diff --git a/app/src/main/java/io/github/wulkanowy/utils/StudentExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/StudentExtension.kt index fdd0610a0..132a3085e 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/StudentExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/StudentExtension.kt @@ -2,4 +2,4 @@ package io.github.wulkanowy.utils import io.github.wulkanowy.data.db.entities.Student -inline val Student.nickOrName get() = if (nick.isBlank()) studentName else nick +inline val Student.nickOrName get() = nick.ifBlank { studentName } diff --git a/app/src/main/java/io/github/wulkanowy/utils/WebkitCookieManagerProxy.kt b/app/src/main/java/io/github/wulkanowy/utils/WebkitCookieManagerProxy.kt new file mode 100644 index 000000000..3d41c711c --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/utils/WebkitCookieManagerProxy.kt @@ -0,0 +1,70 @@ +package io.github.wulkanowy.utils + +import android.util.AndroidRuntimeException +import java.net.CookiePolicy +import java.net.CookieStore +import java.net.HttpCookie +import java.net.URI +import android.webkit.CookieManager as WebkitCookieManager +import java.net.CookieManager as JavaCookieManager + +class WebkitCookieManagerProxy : JavaCookieManager(null, CookiePolicy.ACCEPT_ALL) { + + private val webkitCookieManager: WebkitCookieManager? = getWebkitCookieManager() + + /** + * @see [https://stackoverflow.com/a/70354583/6695449] + */ + private fun getWebkitCookieManager(): WebkitCookieManager? { + return try { + WebkitCookieManager.getInstance() + } catch (e: AndroidRuntimeException) { + null + } + } + + override fun put(uri: URI?, responseHeaders: Map>?) { + if (uri == null || responseHeaders == null) return + val url = uri.toString() + for (headerKey in responseHeaders.keys) { + if (headerKey == null || !( + headerKey.equals("Set-Cookie2", ignoreCase = true) || + headerKey.equals("Set-Cookie", ignoreCase = true) + ) + ) continue + + // process each of the headers + for (headerValue in responseHeaders[headerKey].orEmpty()) { + webkitCookieManager?.setCookie(url, headerValue) + } + } + } + + override operator fun get( + uri: URI?, + requestHeaders: Map?>? + ): Map> { + require(!(uri == null || requestHeaders == null)) { "Argument is null" } + val res = mutableMapOf>() + val cookie = webkitCookieManager?.getCookie(uri.toString()) + if (cookie != null) res["Cookie"] = listOf(cookie) + return res + } + + override fun getCookieStore(): CookieStore { + val cookies = super.getCookieStore() + return object : CookieStore { + override fun add(uri: URI?, cookie: HttpCookie?) = cookies.add(uri, cookie) + override fun get(uri: URI?): List = cookies.get(uri) + override fun getCookies(): List = cookies.cookies + override fun getURIs(): List = cookies.urIs + override fun remove(uri: URI?, cookie: HttpCookie?): Boolean = + cookies.remove(uri, cookie) + + override fun removeAll(): Boolean { + webkitCookieManager?.removeAllCookies(null) ?: return false + return true + } + } + } +} diff --git a/app/src/main/java/io/github/wulkanowy/utils/security/Scrambler.kt b/app/src/main/java/io/github/wulkanowy/utils/security/Scrambler.kt index c994ebab6..db16a2563 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/security/Scrambler.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/security/Scrambler.kt @@ -16,6 +16,7 @@ import android.util.Base64.DEFAULT import android.util.Base64.decode import android.util.Base64.encode import android.util.Base64.encodeToString +import dagger.hilt.android.qualifiers.ApplicationContext import timber.log.Timber import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream @@ -33,108 +34,124 @@ import javax.crypto.CipherInputStream import javax.crypto.CipherOutputStream import javax.crypto.spec.OAEPParameterSpec import javax.crypto.spec.PSource.PSpecified +import javax.inject.Inject +import javax.inject.Singleton import javax.security.auth.x500.X500Principal -private const val KEYSTORE_NAME = "AndroidKeyStore" +@Singleton +class Scrambler @Inject constructor( + @ApplicationContext private val context: Context, +) { + private val keyCharset = Charset.forName("UTF-8") -private const val KEY_ALIAS = "wulkanowy_password" + private val isKeyPairExists: Boolean + get() = keyStore.getKey(KEY_ALIAS, null) != null -private val KEY_CHARSET = Charset.forName("UTF-8") + private val keyStore: KeyStore + get() = KeyStore.getInstance(KEYSTORE_NAME).apply { load(null) } -private val isKeyPairExists: Boolean - get() = keyStore.getKey(KEY_ALIAS, null) != null + private val cipher: Cipher + get() { + return if (SDK_INT >= M) Cipher.getInstance( + "RSA/ECB/OAEPWithSHA-256AndMGF1Padding", + "AndroidKeyStoreBCWorkaround" + ) + else Cipher.getInstance("RSA/ECB/PKCS1Padding", "AndroidOpenSSL") + } -private val keyStore: KeyStore - get() = KeyStore.getInstance(KEYSTORE_NAME).apply { load(null) } + fun encrypt(plainText: String): String { + if (plainText.isEmpty()) throw ScramblerException("Text to be encrypted is empty") -private val cipher: Cipher - get() { - return if (SDK_INT >= M) Cipher.getInstance( - "RSA/ECB/OAEPWithSHA-256AndMGF1Padding", - "AndroidKeyStoreBCWorkaround" - ) - else Cipher.getInstance("RSA/ECB/PKCS1Padding", "AndroidOpenSSL") + return try { + if (!isKeyPairExists) generateKeyPair() + + cipher.let { + if (SDK_INT >= M) { + OAEPParameterSpec("SHA-256", "MGF1", SHA1, PSpecified.DEFAULT).let { spec -> + it.init(ENCRYPT_MODE, keyStore.getCertificate(KEY_ALIAS).publicKey, spec) + } + } else it.init(ENCRYPT_MODE, keyStore.getCertificate(KEY_ALIAS).publicKey) + + ByteArrayOutputStream().let { output -> + CipherOutputStream(output, it).apply { + write(plainText.toByteArray(keyCharset)) + close() + } + encodeToString(output.toByteArray(), DEFAULT) + } + } + } catch (exception: Exception) { + Timber.e(exception, "An error occurred while encrypting text") + String(encode(plainText.toByteArray(keyCharset), DEFAULT), keyCharset) + } } -fun encrypt(plainText: String, context: Context): String { - if (plainText.isEmpty()) throw ScramblerException("Text to be encrypted is empty") + fun decrypt(cipherText: String): String { + if (cipherText.isEmpty()) throw ScramblerException("Text to be encrypted is empty") - return try { - if (!isKeyPairExists) generateKeyPair(context) + return try { + if (!isKeyPairExists) throw ScramblerException("KeyPair doesn't exist") - cipher.let { - if (SDK_INT >= M) { - OAEPParameterSpec("SHA-256", "MGF1", SHA1, PSpecified.DEFAULT).let { spec -> - it.init(ENCRYPT_MODE, keyStore.getCertificate(KEY_ALIAS).publicKey, spec) + cipher.let { + if (SDK_INT >= M) { + OAEPParameterSpec("SHA-256", "MGF1", SHA1, PSpecified.DEFAULT).let { spec -> + it.init(DECRYPT_MODE, keyStore.getKey(KEY_ALIAS, null), spec) + } + } else it.init(DECRYPT_MODE, keyStore.getKey(KEY_ALIAS, null)) + + CipherInputStream( + ByteArrayInputStream(decode(cipherText, DEFAULT)), + it + ).let { input -> + val values = ArrayList() + var nextByte: Int + while (run { nextByte = input.read(); nextByte } != -1) { + values.add(nextByte.toByte()) + } + val bytes = ByteArray(values.size) + for (i in bytes.indices) { + bytes[i] = values[i] + } + String(bytes, 0, bytes.size, keyCharset) } - } else it.init(ENCRYPT_MODE, keyStore.getCertificate(KEY_ALIAS).publicKey) + } + } catch (e: Exception) { + throw ScramblerException("An error occurred while decrypting text", e) + } + } - ByteArrayOutputStream().let { output -> - CipherOutputStream(output, it).apply { - write(plainText.toByteArray(KEY_CHARSET)) - close() - } - encodeToString(output.toByteArray(), DEFAULT) + private fun generateKeyPair() { + (if (SDK_INT >= M) { + KeyGenParameterSpec.Builder(KEY_ALIAS, PURPOSE_DECRYPT or PURPOSE_ENCRYPT) + .setDigests(DIGEST_SHA256, DIGEST_SHA512) + .setEncryptionPaddings(ENCRYPTION_PADDING_RSA_OAEP) + .setCertificateSerialNumber(BigInteger.TEN) + .setCertificateSubject(X500Principal("CN=Wulkanowy")) + .build() + } else { + KeyPairGeneratorSpec.Builder(context) + .setAlias(KEY_ALIAS) + .setSubject(X500Principal("CN=Wulkanowy")) + .setSerialNumber(BigInteger.TEN) + .setStartDate(Calendar.getInstance().time) + .setEndDate(Calendar.getInstance().apply { add(YEAR, 99) }.time) + .build() + }).let { + KeyPairGenerator.getInstance("RSA", KEYSTORE_NAME).apply { + initialize(it) + genKeyPair() } } - } catch (exception: Exception) { - Timber.e(exception, "An error occurred while encrypting text") - String(encode(plainText.toByteArray(KEY_CHARSET), DEFAULT), KEY_CHARSET) + Timber.i("A new KeyPair has been generated") + } + + fun clearKeyPair() { + keyStore.deleteEntry(KEY_ALIAS) + Timber.i("KeyPair has been cleared") + } + + private companion object { + private const val KEYSTORE_NAME = "AndroidKeyStore" + private const val KEY_ALIAS = "wulkanowy_password" } } - -fun decrypt(cipherText: String): String { - if (cipherText.isEmpty()) throw ScramblerException("Text to be encrypted is empty") - - return try { - if (!isKeyPairExists) throw ScramblerException("KeyPair doesn't exist") - - cipher.let { - if (SDK_INT >= M) { - OAEPParameterSpec("SHA-256", "MGF1", SHA1, PSpecified.DEFAULT).let { spec -> - it.init(DECRYPT_MODE, keyStore.getKey(KEY_ALIAS, null), spec) - } - } else it.init(DECRYPT_MODE, keyStore.getKey(KEY_ALIAS, null)) - - CipherInputStream(ByteArrayInputStream(decode(cipherText, DEFAULT)), it).let { input -> - val values = ArrayList() - var nextByte: Int - while (run { nextByte = input.read(); nextByte } != -1) { - values.add(nextByte.toByte()) - } - val bytes = ByteArray(values.size) - for (i in bytes.indices) { - bytes[i] = values[i] - } - String(bytes, 0, bytes.size, KEY_CHARSET) - } - } - } catch (e: Exception) { - throw ScramblerException("An error occurred while decrypting text", e) - } -} - -private fun generateKeyPair(context: Context) { - (if (SDK_INT >= M) { - KeyGenParameterSpec.Builder(KEY_ALIAS, PURPOSE_DECRYPT or PURPOSE_ENCRYPT) - .setDigests(DIGEST_SHA256, DIGEST_SHA512) - .setEncryptionPaddings(ENCRYPTION_PADDING_RSA_OAEP) - .setCertificateSerialNumber(BigInteger.TEN) - .setCertificateSubject(X500Principal("CN=Wulkanowy")) - .build() - } else { - KeyPairGeneratorSpec.Builder(context) - .setAlias(KEY_ALIAS) - .setSubject(X500Principal("CN=Wulkanowy")) - .setSerialNumber(BigInteger.TEN) - .setStartDate(Calendar.getInstance().time) - .setEndDate(Calendar.getInstance().apply { add(YEAR, 99) }.time) - .build() - }).let { - KeyPairGenerator.getInstance("RSA", KEYSTORE_NAME).apply { - initialize(it) - genKeyPair() - } - } - Timber.i("A new KeyPair has been generated") -} diff --git a/app/src/main/play/release-notes/pl-PL/default.txt b/app/src/main/play/release-notes/pl-PL/default.txt index 064401abd..5736992bf 100644 --- a/app/src/main/play/release-notes/pl-PL/default.txt +++ b/app/src/main/play/release-notes/pl-PL/default.txt @@ -1,7 +1,5 @@ -Wersja 1.7.5 +Wersja 2.4.1 -- 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 +- drobne poprawki stabilności aplikacji i odświeżania danych Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases diff --git a/app/src/main/res/drawable-night/background_header_note.xml b/app/src/main/res/drawable-night/background_header_note.xml deleted file mode 100644 index 6b594e7c6..000000000 --- a/app/src/main/res/drawable-night/background_header_note.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/background_grade_details_rounded.xml b/app/src/main/res/drawable/background_grade_details_rounded.xml new file mode 100644 index 000000000..e24088a0c --- /dev/null +++ b/app/src/main/res/drawable/background_grade_details_rounded.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/background_grade_details_weight_rounded.xml b/app/src/main/res/drawable/background_grade_details_weight_rounded.xml new file mode 100644 index 000000000..4b2109128 --- /dev/null +++ b/app/src/main/res/drawable/background_grade_details_weight_rounded.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/background_widget_timetable_dark.xml b/app/src/main/res/drawable/background_grade_rounded.xml similarity index 74% rename from app/src/main/res/drawable/background_widget_timetable_dark.xml rename to app/src/main/res/drawable/background_grade_rounded.xml index 6fe7d0ab2..52c10c2f4 100644 --- a/app/src/main/res/drawable/background_widget_timetable_dark.xml +++ b/app/src/main/res/drawable/background_grade_rounded.xml @@ -1,5 +1,5 @@ - + - \ No newline at end of file + diff --git a/app/src/main/res/drawable/background_widget_item_timetable_dark.xml b/app/src/main/res/drawable/background_grade_small_rounded.xml similarity index 51% rename from app/src/main/res/drawable/background_widget_item_timetable_dark.xml rename to app/src/main/res/drawable/background_grade_small_rounded.xml index e432a648c..dd50417f7 100644 --- a/app/src/main/res/drawable/background_widget_item_timetable_dark.xml +++ b/app/src/main/res/drawable/background_grade_small_rounded.xml @@ -1,5 +1,5 @@ - - - \ No newline at end of file + + + diff --git a/app/src/main/res/drawable/background_header_note.xml b/app/src/main/res/drawable/background_header_note.xml index c21e55c6b..8cf84a1cd 100644 --- a/app/src/main/res/drawable/background_header_note.xml +++ b/app/src/main/res/drawable/background_header_note.xml @@ -1,5 +1,5 @@ - + diff --git a/app/src/main/res/drawable/background_luckynumber_widget.xml b/app/src/main/res/drawable/background_luckynumber_widget_button.xml similarity index 65% rename from app/src/main/res/drawable/background_luckynumber_widget.xml rename to app/src/main/res/drawable/background_luckynumber_widget_button.xml index 367c55275..66b1685f6 100644 --- a/app/src/main/res/drawable/background_luckynumber_widget.xml +++ b/app/src/main/res/drawable/background_luckynumber_widget_button.xml @@ -1,6 +1,6 @@ - - + + diff --git a/app/src/main/res/drawable/background_luckynumber_widget_dark.xml b/app/src/main/res/drawable/background_luckynumber_widget_dark.xml deleted file mode 100644 index cb094b57e..000000000 --- a/app/src/main/res/drawable/background_luckynumber_widget_dark.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/background_material_alert_dialog.xml b/app/src/main/res/drawable/background_material_alert_dialog.xml new file mode 100644 index 000000000..5ab8a3506 --- /dev/null +++ b/app/src/main/res/drawable/background_material_alert_dialog.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/background_timetable_widget_avatar.xml b/app/src/main/res/drawable/background_timetable_widget_avatar.xml new file mode 100644 index 000000000..48298d675 --- /dev/null +++ b/app/src/main/res/drawable/background_timetable_widget_avatar.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/drawable/background_widget_header_timetable_dark.xml b/app/src/main/res/drawable/background_widget_header_timetable_dark.xml deleted file mode 100644 index 616a91279..000000000 --- a/app/src/main/res/drawable/background_widget_header_timetable_dark.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/background_widget_item_timetable.xml b/app/src/main/res/drawable/background_widget_item_timetable.xml index 08854fba2..510c70c0c 100644 --- a/app/src/main/res/drawable/background_widget_item_timetable.xml +++ b/app/src/main/res/drawable/background_widget_item_timetable.xml @@ -1,5 +1,5 @@ - - - \ No newline at end of file + + + diff --git a/app/src/main/res/drawable/background_widget_timetable.xml b/app/src/main/res/drawable/background_widget_timetable.xml index 2267587d9..b589ad29d 100644 --- a/app/src/main/res/drawable/background_widget_timetable.xml +++ b/app/src/main/res/drawable/background_widget_timetable.xml @@ -1,5 +1,5 @@ - - - \ No newline at end of file + + + diff --git a/app/src/main/res/drawable/ic_auth_success.xml b/app/src/main/res/drawable/ic_auth_success.xml new file mode 100644 index 000000000..015553c02 --- /dev/null +++ b/app/src/main/res/drawable/ic_auth_success.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_chevron_left.xml b/app/src/main/res/drawable/ic_chevron_left.xml index ee3ff4be8..4250fae47 100644 --- a/app/src/main/res/drawable/ic_chevron_left.xml +++ b/app/src/main/res/drawable/ic_chevron_left.xml @@ -1,5 +1,10 @@ - - + + diff --git a/app/src/main/res/drawable/ic_chevron_right.xml b/app/src/main/res/drawable/ic_chevron_right.xml index a6d734973..de5037cf0 100644 --- a/app/src/main/res/drawable/ic_chevron_right.xml +++ b/app/src/main/res/drawable/ic_chevron_right.xml @@ -1,5 +1,10 @@ - - + + diff --git a/app/src/main/res/drawable/background_widget_header_timetable.xml b/app/src/main/res/drawable/ic_circle.xml similarity index 57% rename from app/src/main/res/drawable/background_widget_header_timetable.xml rename to app/src/main/res/drawable/ic_circle.xml index 98eec700d..d2932fe62 100644 --- a/app/src/main/res/drawable/background_widget_header_timetable.xml +++ b/app/src/main/res/drawable/ic_circle.xml @@ -1,7 +1,10 @@ - + + - - \ No newline at end of file + + + diff --git a/app/src/main/res/drawable/ic_close.xml b/app/src/main/res/drawable/ic_close.xml new file mode 100644 index 000000000..1d6c00461 --- /dev/null +++ b/app/src/main/res/drawable/ic_close.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_error_filled.xml b/app/src/main/res/drawable/ic_error_filled.xml new file mode 100644 index 000000000..61b575dc6 --- /dev/null +++ b/app/src/main/res/drawable/ic_error_filled.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_history.xml b/app/src/main/res/drawable/ic_history.xml new file mode 100644 index 000000000..f20e2094b --- /dev/null +++ b/app/src/main/res/drawable/ic_history.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground_dev_mono.xml b/app/src/main/res/drawable/ic_launcher_foreground_dev_mono.xml new file mode 100644 index 000000000..b1b01a0b6 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground_dev_mono.xml @@ -0,0 +1,20 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground_mono.xml b/app/src/main/res/drawable/ic_launcher_foreground_mono.xml new file mode 100644 index 000000000..e2e747316 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground_mono.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_login.xml b/app/src/main/res/drawable/ic_login.xml new file mode 100644 index 000000000..57aef3dca --- /dev/null +++ b/app/src/main/res/drawable/ic_login.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_menu_order_drag.xml b/app/src/main/res/drawable/ic_menu_order_drag.xml new file mode 100644 index 000000000..623c67d2e --- /dev/null +++ b/app/src/main/res/drawable/ic_menu_order_drag.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_scale_balance.xml b/app/src/main/res/drawable/ic_scale_balance.xml new file mode 100644 index 000000000..c65467a6c --- /dev/null +++ b/app/src/main/res/drawable/ic_scale_balance.xml @@ -0,0 +1,7 @@ + + + diff --git a/app/src/main/res/drawable/ic_timetable_widget_swap.xml b/app/src/main/res/drawable/ic_timetable_widget_swap.xml new file mode 100644 index 000000000..09c50b4f1 --- /dev/null +++ b/app/src/main/res/drawable/ic_timetable_widget_swap.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_widget_chevron.png b/app/src/main/res/drawable/ic_widget_chevron.png deleted file mode 100644 index 34345521a..000000000 Binary files a/app/src/main/res/drawable/ic_widget_chevron.png and /dev/null differ diff --git a/app/src/main/res/drawable/ic_widget_chevron.xml b/app/src/main/res/drawable/ic_widget_chevron.xml new file mode 100644 index 000000000..871aca9bf --- /dev/null +++ b/app/src/main/res/drawable/ic_widget_chevron.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/img_luckynumber_widget_preview.png b/app/src/main/res/drawable/img_luckynumber_widget_preview.png index 539b0a598..267018550 100644 Binary files a/app/src/main/res/drawable/img_luckynumber_widget_preview.png and b/app/src/main/res/drawable/img_luckynumber_widget_preview.png differ diff --git a/app/src/main/res/drawable/img_timetable_widget_preview.png b/app/src/main/res/drawable/img_timetable_widget_preview.png old mode 100755 new mode 100644 index 849430187..5c1752cb5 Binary files a/app/src/main/res/drawable/img_timetable_widget_preview.png and b/app/src/main/res/drawable/img_timetable_widget_preview.png differ diff --git a/app/src/main/res/drawable/shape_badge.xml b/app/src/main/res/drawable/shape_badge.xml new file mode 100644 index 000000000..3153f592d --- /dev/null +++ b/app/src/main/res/drawable/shape_badge.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout-v31/widget_timetable_preview.xml b/app/src/main/res/layout-v31/widget_timetable_preview.xml new file mode 100644 index 000000000..2c47836cc --- /dev/null +++ b/app/src/main/res/layout-v31/widget_timetable_preview.xml @@ -0,0 +1,347 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 11844e244..d14de50a1 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,24 +1,31 @@ - + android:fitsSystemWindows="true" + app:layout_constraintBaseline_toTopOf="parent"> + + + + app:layout_constraintTop_toBottomOf="@id/main_app_bar" + tools:layout="@layout/fragment_dashboard" /> - + android:fitsSystemWindows="true" + app:layout_constraintBaseline_toTopOf="parent"> + + + + app:layout_constraintTop_toBottomOf="@id/send_app_bar"> + android:layout_height="wrap_content"> + + + app:layout_constraintTop_toBottomOf="@id/send_app_bar" /> diff --git a/app/src/main/res/layout/dialog_account_edit.xml b/app/src/main/res/layout/dialog_account_edit.xml index 9f617e440..2ab4ccc6a 100644 --- a/app/src/main/res/layout/dialog_account_edit.xml +++ b/app/src/main/res/layout/dialog_account_edit.xml @@ -1,19 +1,14 @@ - - + android:layout_height="match_parent"> - + android:paddingStart="24dp" + android:paddingEnd="24dp"> + android:textAppearance="?attr/textAppearanceHeadlineSmall" + android:textColor="?attr/colorOnSurface" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="0" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> - + android:text="@string/additional_lessons_repeat" + app:layout_constraintTop_toBottomOf="@id/additionalLessonDialogDate" /> + android:hint="@string/all_subject" + app:layout_constraintTop_toBottomOf="@id/additionalLessonDialogEnd"> - + - - - - - + + diff --git a/app/src/main/res/layout/dialog_ads_consent.xml b/app/src/main/res/layout/dialog_ads_consent.xml deleted file mode 100644 index 816074783..000000000 --- a/app/src/main/res/layout/dialog_ads_consent.xml +++ /dev/null @@ -1,79 +0,0 @@ - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/dialog_attendance.xml b/app/src/main/res/layout/dialog_attendance.xml index 08ed9d4a3..d832490a7 100644 --- a/app/src/main/res/layout/dialog_attendance.xml +++ b/app/src/main/res/layout/dialog_attendance.xml @@ -5,28 +5,21 @@ android:layout_height="match_parent"> - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_captcha.xml b/app/src/main/res/layout/dialog_captcha.xml new file mode 100644 index 000000000..539aa0cc9 --- /dev/null +++ b/app/src/main/res/layout/dialog_captcha.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_conference.xml b/app/src/main/res/layout/dialog_conference.xml index d08edf4f7..72837b819 100644 --- a/app/src/main/res/layout/dialog_conference.xml +++ b/app/src/main/res/layout/dialog_conference.xml @@ -6,28 +6,20 @@ android:layout_height="match_parent"> - - + android:layout_height="match_parent"> - - + app:layout_constraintEnd_toStartOf="@+id/examDialogClose" /> + android:layout_height="wrap_content" + app:counterEnabled="true" + app:counterMaxLength="256"> + android:layout_weight="1" + android:hint="@string/attendance_excuse_dialog_reason" + android:maxLength="256" /> diff --git a/app/src/main/res/layout/dialog_grade.xml b/app/src/main/res/layout/dialog_grade.xml index 9c52c1d0b..f47f61088 100644 --- a/app/src/main/res/layout/dialog_grade.xml +++ b/app/src/main/res/layout/dialog_grade.xml @@ -21,7 +21,7 @@ - + android:background="@drawable/background_grade_details_weight_rounded" + android:backgroundTint="@color/grade_black" + android:gravity="center_horizontal"> + + + + + + android:textAppearance="?attr/textAppearanceHeadlineSmall" + android:textColor="?attr/colorOnSurface" + android:textIsSelectable="true" /> + android:text="@string/all_no_description" + android:textAppearance="?attr/textAppearanceBodyLarge" + android:textColor="?attr/colorOnSurface" + android:textIsSelectable="true" /> + android:textAppearance="?attr/textAppearanceBodySmall" + android:textColor="?attr/colorOnSurfaceVariant" /> + android:textAppearance="?attr/textAppearanceBodyLarge" + android:textColor="?attr/colorOnSurface" + android:textIsSelectable="true" /> + android:textAppearance="?attr/textAppearanceBodySmall" + android:textColor="?attr/colorOnSurfaceVariant" /> + android:textAppearance="?attr/textAppearanceBodyLarge" + android:textColor="?attr/colorOnSurface" + android:textIsSelectable="true" /> + android:textAppearance="?attr/textAppearanceBodySmall" + android:textColor="?attr/colorOnSurfaceVariant" /> + android:textAppearance="?attr/textAppearanceBodyLarge" + android:textColor="?attr/colorOnSurface" + android:textIsSelectable="true" /> + android:textAppearance="?attr/textAppearanceBodySmall" + android:textColor="?attr/colorOnSurfaceVariant" /> + android:textAppearance="?attr/textAppearanceBodyLarge" + android:textColor="?attr/colorOnSurface" + android:textIsSelectable="true" /> - - diff --git a/app/src/main/res/layout/dialog_homework.xml b/app/src/main/res/layout/dialog_homework.xml index 341cec544..8c6cf0a76 100644 --- a/app/src/main/res/layout/dialog_homework.xml +++ b/app/src/main/res/layout/dialog_homework.xml @@ -1,71 +1,56 @@ - + android:layout_height="wrap_content"> + android:background="@drawable/ic_all_divider" + app:layout_constraintTop_toBottomOf="@id/homeworkDialogRecycler" /> - + - - - - - + + diff --git a/app/src/main/res/layout/dialog_homework_add.xml b/app/src/main/res/layout/dialog_homework_add.xml index 524f0db0d..e0ff5b749 100644 --- a/app/src/main/res/layout/dialog_homework_add.xml +++ b/app/src/main/res/layout/dialog_homework_add.xml @@ -2,37 +2,35 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" - android:layout_height="match_parent" - android:fillViewport="true" - android:minWidth="300dp" - android:paddingStart="8dp" - android:paddingEnd="8dp"> + android:layout_height="match_parent"> - + android:paddingStart="24dp" + android:paddingEnd="24dp"> + android:textAppearance="?attr/textAppearanceHeadlineSmall" + android:textColor="?attr/colorOnSurface" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="0" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + android:hint="@string/all_subject" + app:layout_constraintTop_toBottomOf="@id/homeworkDialogDate"> + android:hint="@string/all_teacher" + app:layout_constraintTop_toBottomOf="@id/homeworkDialogSubject"> + android:hint="@string/all_content" + app:layout_constraintTop_toBottomOf="@id/homeworkDialogTeacher"> - + - - - - - + + diff --git a/app/src/main/res/layout/dialog_lesson_completed.xml b/app/src/main/res/layout/dialog_lesson_completed.xml index 500cdb6f3..3a1d3fd00 100644 --- a/app/src/main/res/layout/dialog_lesson_completed.xml +++ b/app/src/main/res/layout/dialog_lesson_completed.xml @@ -1,4 +1,5 @@ + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_mailbox_chooser.xml b/app/src/main/res/layout/dialog_mailbox_chooser.xml new file mode 100644 index 000000000..5c93acb59 --- /dev/null +++ b/app/src/main/res/layout/dialog_mailbox_chooser.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_mobile_device.xml b/app/src/main/res/layout/dialog_mobile_device.xml index 043afb2ad..9b81737fb 100644 --- a/app/src/main/res/layout/dialog_mobile_device.xml +++ b/app/src/main/res/layout/dialog_mobile_device.xml @@ -7,18 +7,10 @@ tools:context=".ui.modules.mobiledevice.token.MobileDeviceTokenDialog"> - - + - - + - - - - + app:layout_constraintTop_toBottomOf="@id/timetableDialogLessonValue" + tools:visibility="visible" /> + app:layout_constraintTop_toBottomOf="@id/timetableDialogTeacherValue" + tools:visibility="visible" /> + app:layout_constraintTop_toBottomOf="@id/timetableDialogRoomValue" + tools:visibility="visible" /> @@ -106,7 +105,7 @@ + android:visibility="gone" + tools:ignore="UnusedAttribute" + tools:visibility="visible"> + android:layout_height="match_parent" + tools:visibility="gone"> + android:paddingBottom="6dp" + tools:listitem="@layout/item_dashboard_grades" /> - + + @@ -54,18 +70,25 @@ android:gravity="center" android:padding="8dp" android:text="@string/error_unknown" - android:textSize="20sp" /> + android:textSize="20sp" + app:layout_constraintBottom_toTopOf="@id/dashboard_error_buttons" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/dashboard_error_image" /> + android:orientation="horizontal" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintTop_toBottomOf="@id/dashboard_error_message"> - - \ No newline at end of file + + diff --git a/app/src/main/res/layout/fragment_exam.xml b/app/src/main/res/layout/fragment_exam.xml index 0c62aab5f..8dba5361b 100644 --- a/app/src/main/res/layout/fragment_exam.xml +++ b/app/src/main/res/layout/fragment_exam.xml @@ -91,7 +91,7 @@ diff --git a/app/src/main/res/layout/fragment_login_advanced.xml b/app/src/main/res/layout/fragment_login_advanced.xml index c7acaa70b..37551decb 100644 --- a/app/src/main/res/layout/fragment_login_advanced.xml +++ b/app/src/main/res/layout/fragment_login_advanced.xml @@ -97,7 +97,7 @@ @@ -185,9 +185,34 @@ tools:ignore="Deprecated,LabelFor" /> + + + + + + app:layout_constraintTop_toBottomOf="@+id/loginFormDomainSuffixLayout"> + + + app:layout_constraintTop_toBottomOf="@+id/loginFormUsernameLayout"> + + + + + + app:layout_constraintTop_toBottomOf="@+id/loginFormDomainSuffixLayout" /> - - + android:orientation="vertical" + tools:context=".ui.modules.login.studentselect.LoginStudentSelectFragment"> - - - - - - - - - - - - - - - - + tools:itemCount="33" + tools:listitem="@layout/item_login_student_select_student" /> + + diff --git a/app/src/main/res/layout/fragment_login_symbol.xml b/app/src/main/res/layout/fragment_login_symbol.xml index f1c9be4a0..37f4c731a 100644 --- a/app/src/main/res/layout/fragment_login_symbol.xml +++ b/app/src/main/res/layout/fragment_login_symbol.xml @@ -66,7 +66,7 @@ + app:layout_constraintTop_toBottomOf="@+id/loginSymbolHelper" + app:placeholderText="@string/login_symbol_placeholder" + app:placeholderTextColor="?colorTertiary"> + + + + diff --git a/app/src/main/res/layout/fragment_message.xml b/app/src/main/res/layout/fragment_message.xml index 5269d95e8..b3dac0dd1 100644 --- a/app/src/main/res/layout/fragment_message.xml +++ b/app/src/main/res/layout/fragment_message.xml @@ -29,6 +29,7 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_notifications_center.xml b/app/src/main/res/layout/fragment_notifications_center.xml index f59ce33c9..ba2f0ff49 100644 --- a/app/src/main/res/layout/fragment_notifications_center.xml +++ b/app/src/main/res/layout/fragment_notifications_center.xml @@ -89,7 +89,7 @@ + android:visibility="gone" + tools:ignore="UnusedAttribute" + tools:visibility="visible"> + android:layout_height="0dp" + android:layout_weight="1"> @@ -173,4 +173,4 @@ app:srcCompat="@drawable/ic_chevron_right" app:tint="?colorPrimary" /> - + diff --git a/app/src/main/res/layout/fragment_timetable_completed.xml b/app/src/main/res/layout/fragment_timetable_completed.xml index e089275d9..8d647ff61 100644 --- a/app/src/main/res/layout/fragment_timetable_completed.xml +++ b/app/src/main/res/layout/fragment_timetable_completed.xml @@ -93,7 +93,7 @@ + android:layout_marginVertical="6dp"> + tools:text="Szkoła Wulkanowego" /> - \ No newline at end of file + diff --git a/app/src/main/res/layout/item_dashboard_admin_message.xml b/app/src/main/res/layout/item_dashboard_admin_message.xml index 67836561b..407e12921 100644 --- a/app/src/main/res/layout/item_dashboard_admin_message.xml +++ b/app/src/main/res/layout/item_dashboard_admin_message.xml @@ -5,8 +5,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginHorizontal="12dp" - android:layout_marginVertical="6dp" - app:cardElevation="4dp"> + android:layout_marginVertical="6dp"> + + - \ No newline at end of file + diff --git a/app/src/main/res/layout/item_dashboard_announcements.xml b/app/src/main/res/layout/item_dashboard_announcements.xml index 19f720884..b9ddb7575 100644 --- a/app/src/main/res/layout/item_dashboard_announcements.xml +++ b/app/src/main/res/layout/item_dashboard_announcements.xml @@ -8,8 +8,7 @@ android:layout_marginVertical="6dp" android:clickable="true" android:focusable="true" - android:foreground="?selectableItemBackground" - app:cardElevation="4dp"> + android:foreground="?selectableItemBackground"> - \ No newline at end of file + diff --git a/app/src/main/res/layout/item_dashboard_conferences.xml b/app/src/main/res/layout/item_dashboard_conferences.xml index 02d3edfc8..b02b8e18f 100644 --- a/app/src/main/res/layout/item_dashboard_conferences.xml +++ b/app/src/main/res/layout/item_dashboard_conferences.xml @@ -8,8 +8,7 @@ android:layout_marginVertical="6dp" android:clickable="true" android:focusable="true" - android:foreground="?selectableItemBackground" - app:cardElevation="4dp"> + android:foreground="?selectableItemBackground"> - \ No newline at end of file + diff --git a/app/src/main/res/layout/item_dashboard_exams.xml b/app/src/main/res/layout/item_dashboard_exams.xml index 9cc98d790..84302403c 100644 --- a/app/src/main/res/layout/item_dashboard_exams.xml +++ b/app/src/main/res/layout/item_dashboard_exams.xml @@ -8,8 +8,7 @@ android:layout_marginVertical="6dp" android:clickable="true" android:focusable="true" - android:foreground="?selectableItemBackground" - app:cardElevation="4dp"> + android:foreground="?selectableItemBackground"> - \ No newline at end of file + diff --git a/app/src/main/res/layout/item_dashboard_grades.xml b/app/src/main/res/layout/item_dashboard_grades.xml index 5cc9ce308..345d8a5e4 100644 --- a/app/src/main/res/layout/item_dashboard_grades.xml +++ b/app/src/main/res/layout/item_dashboard_grades.xml @@ -6,8 +6,7 @@ android:layout_height="wrap_content" android:layout_marginHorizontal="12dp" android:layout_marginVertical="6dp" - android:foreground="?selectableItemBackground" - app:cardElevation="4dp"> + android:foreground="?selectableItemBackground"> - \ No newline at end of file + diff --git a/app/src/main/res/layout/item_dashboard_homework.xml b/app/src/main/res/layout/item_dashboard_homework.xml index 975d66efd..b36afc570 100644 --- a/app/src/main/res/layout/item_dashboard_homework.xml +++ b/app/src/main/res/layout/item_dashboard_homework.xml @@ -8,8 +8,7 @@ android:layout_marginVertical="6dp" android:clickable="true" android:focusable="true" - android:foreground="?selectableItemBackground" - app:cardElevation="4dp"> + android:foreground="?selectableItemBackground"> - \ No newline at end of file + diff --git a/app/src/main/res/layout/item_dashboard_horizontal_group.xml b/app/src/main/res/layout/item_dashboard_horizontal_group.xml index 1d43d5115..1c9246a19 100644 --- a/app/src/main/res/layout/item_dashboard_horizontal_group.xml +++ b/app/src/main/res/layout/item_dashboard_horizontal_group.xml @@ -13,7 +13,6 @@ android:layout_width="0dp" android:layout_height="44dp" android:layout_marginVertical="4dp" - app:cardElevation="4dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@id/dashboard_horizontal_group_item_message_container" app:layout_constraintHorizontal_chainStyle="spread_inside" @@ -37,9 +36,25 @@ app:layout_constraintHorizontal_chainStyle="packed" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" + app:layout_goneMarginEnd="16dp" app:tint="?colorOnSurface" tools:ignore="ContentDescription" /> + + + + + tools:text="16" + tools:visibility="visible" /> @@ -121,7 +152,6 @@ android:layout_width="0dp" android:layout_height="44dp" android:layout_marginVertical="4dp" - app:cardElevation="4dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@id/dashboard_horizontal_group_item_message_container" @@ -145,9 +175,25 @@ app:layout_constraintHorizontal_chainStyle="packed" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" + app:layout_goneMarginEnd="16dp" app:tint="?colorOnSurface" tools:ignore="ContentDescription" /> + + - \ No newline at end of file + diff --git a/app/src/main/res/layout/item_dashboard_lessons.xml b/app/src/main/res/layout/item_dashboard_lessons.xml index 9156c1a2f..a40f17f22 100644 --- a/app/src/main/res/layout/item_dashboard_lessons.xml +++ b/app/src/main/res/layout/item_dashboard_lessons.xml @@ -5,11 +5,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginHorizontal="12dp" - android:layout_marginVertical="6dp" - android:clickable="true" - android:focusable="true" - android:foreground="?selectableItemBackground" - app:cardElevation="4dp"> + android:layout_marginVertical="6dp"> - \ No newline at end of file + diff --git a/app/src/main/res/layout/item_grade_details.xml b/app/src/main/res/layout/item_grade_details.xml index 2f3bd2de5..6849e929f 100644 --- a/app/src/main/res/layout/item_grade_details.xml +++ b/app/src/main/res/layout/item_grade_details.xml @@ -20,11 +20,12 @@ android:id="@+id/gradeItemValue" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:background="@color/grade_material_default" + android:background="@drawable/background_grade_rounded" + android:backgroundTint="@color/grade_material_default" android:gravity="center" android:maxLength="5" android:minWidth="45dp" - android:minHeight="40dp" + android:minHeight="45dp" android:textColor="@android:color/white" android:textSize="16sp" app:layout_constraintBottom_toBottomOf="@+id/gradeDetailsContainer" diff --git a/app/src/main/res/layout/item_grade_statistics_header.xml b/app/src/main/res/layout/item_grade_statistics_header.xml index 92f522ba4..cc35f6066 100644 --- a/app/src/main/res/layout/item_grade_statistics_header.xml +++ b/app/src/main/res/layout/item_grade_statistics_header.xml @@ -1,8 +1,10 @@ + android:layout_height="wrap_content" + tools:context=".ui.modules.grade.statistics.GradeStatisticsAdapter"> - + android:paddingEnd="16dp" + app:selectionRequired="true" + app:singleSelection="true"> - - - - + - \ No newline at end of file + diff --git a/app/src/main/res/layout/item_grade_summary.xml b/app/src/main/res/layout/item_grade_summary.xml index 30aa6e77b..2c8c4ea37 100644 --- a/app/src/main/res/layout/item_grade_summary.xml +++ b/app/src/main/res/layout/item_grade_summary.xml @@ -64,10 +64,12 @@ + android:layout_height="1dp" + android:id="@+id/gradeSummaryItemPointsDivider" + android:background="@drawable/ic_all_divider" /> + android:id="@+id/gradeSummaryItemPredictedDivider" + android:layout_height="1dp" + android:background="@drawable/ic_all_divider" /> + android:layout_height="1dp" + android:id="@+id/gradeSummaryItemFinalDivider" + android:background="@drawable/ic_all_divider" /> + + + + + + + diff --git a/app/src/main/res/layout/item_homework_dialog_details.xml b/app/src/main/res/layout/item_homework_dialog_details.xml index 9d560ba51..a40b5dcee 100644 --- a/app/src/main/res/layout/item_homework_dialog_details.xml +++ b/app/src/main/res/layout/item_homework_dialog_details.xml @@ -13,16 +13,14 @@ android:orientation="horizontal"> + android:textAppearance="?attr/textAppearanceHeadlineSmall" + android:textColor="?attr/colorOnSurface" /> - - - - + + + + + + + + + diff --git a/app/src/main/res/layout/item_login_student_select_header_school.xml b/app/src/main/res/layout/item_login_student_select_header_school.xml new file mode 100644 index 000000000..30a8bbf0b --- /dev/null +++ b/app/src/main/res/layout/item_login_student_select_header_school.xml @@ -0,0 +1,38 @@ + + + + + + + + + + diff --git a/app/src/main/res/layout/item_login_student_select_header_symbol.xml b/app/src/main/res/layout/item_login_student_select_header_symbol.xml new file mode 100644 index 000000000..cc1bf709d --- /dev/null +++ b/app/src/main/res/layout/item_login_student_select_header_symbol.xml @@ -0,0 +1,38 @@ + + + + + + + + + + diff --git a/app/src/main/res/layout/item_login_student_select_help.xml b/app/src/main/res/layout/item_login_student_select_help.xml new file mode 100644 index 000000000..8b04aed22 --- /dev/null +++ b/app/src/main/res/layout/item_login_student_select_help.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_login_student_select.xml b/app/src/main/res/layout/item_login_student_select_student.xml similarity index 71% rename from app/src/main/res/layout/item_login_student_select.xml rename to app/src/main/res/layout/item_login_student_select_student.xml index 1003636fc..d071b1bbf 100644 --- a/app/src/main/res/layout/item_login_student_select.xml +++ b/app/src/main/res/layout/item_login_student_select_student.xml @@ -4,7 +4,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:background="?selectableItemBackground" - android:minHeight="72dp" + android:minHeight="56dp" android:paddingTop="8dp" android:paddingBottom="8dp" tools:context=".ui.modules.login.studentselect.LoginStudentSelectAdapter"> @@ -14,9 +14,10 @@ android:layout_width="32dp" android:layout_height="24dp" android:layout_centerVertical="true" - android:layout_marginStart="12dp" + android:layout_marginStart="32dp" android:layout_marginEnd="28dp" android:background="@android:color/transparent" + android:clickable="false" tools:text=" " /> - - + android:textSize="14sp" /> diff --git a/app/src/main/res/layout/item_mailbox_chooser.xml b/app/src/main/res/layout/item_mailbox_chooser.xml new file mode 100644 index 000000000..7c93199bc --- /dev/null +++ b/app/src/main/res/layout/item_mailbox_chooser.xml @@ -0,0 +1,43 @@ + + + + + + + + diff --git a/app/src/main/res/layout/item_menu_order.xml b/app/src/main/res/layout/item_menu_order.xml new file mode 100644 index 000000000..84510d1ed --- /dev/null +++ b/app/src/main/res/layout/item_menu_order.xml @@ -0,0 +1,46 @@ + + + + + + + + + diff --git a/app/src/main/res/layout/item_message.xml b/app/src/main/res/layout/item_message.xml index c25faacc8..39fbaad01 100644 --- a/app/src/main/res/layout/item_message.xml +++ b/app/src/main/res/layout/item_message.xml @@ -30,6 +30,7 @@ android:layout_marginEnd="10dp" android:ellipsize="end" android:singleLine="true" + android:textColor="?android:textColorSecondary" android:textSize="15sp" app:layout_constraintEnd_toStartOf="@+id/messageItemDate" app:layout_constraintStart_toEndOf="@id/messageItemCheckbox" @@ -40,10 +41,13 @@ android:id="@+id/messageItemDate" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:layout_marginEnd="8dp" android:gravity="end" + android:textColor="?android:textColorSecondary" android:textSize="13sp" - app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintEnd_toStartOf="@id/messageItemUnreadIndicator" app:layout_constraintTop_toTopOf="parent" + app:layout_goneMarginEnd="0dp" tools:text="@tools:sample/date/ddmmyy" /> + + diff --git a/app/src/main/res/layout/item_message_chips.xml b/app/src/main/res/layout/item_message_chips.xml index 481a94835..c1f36c4d3 100644 --- a/app/src/main/res/layout/item_message_chips.xml +++ b/app/src/main/res/layout/item_message_chips.xml @@ -1,40 +1,43 @@ - + app:layout_constraintTop_toTopOf="parent" + app:singleLine="true"> + + + android:text="@string/message_chip_only_unread" /> + android:text="@string/message_chip_only_with_attachments" /> - + diff --git a/app/src/main/res/layout/item_notifications_center.xml b/app/src/main/res/layout/item_notifications_center.xml index 16a7ae0c0..a2a677488 100644 --- a/app/src/main/res/layout/item_notifications_center.xml +++ b/app/src/main/res/layout/item_notifications_center.xml @@ -5,8 +5,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginHorizontal="8dp" - android:layout_marginTop="12dp" - app:cardElevation="4dp"> + android:layout_marginTop="12dp"> - \ No newline at end of file + diff --git a/app/src/main/res/layout/item_timetable_empty.xml b/app/src/main/res/layout/item_timetable_empty.xml new file mode 100644 index 000000000..12fddb752 --- /dev/null +++ b/app/src/main/res/layout/item_timetable_empty.xml @@ -0,0 +1,43 @@ + + + + + + + diff --git a/app/src/main/res/layout/item_widget_timetable.xml b/app/src/main/res/layout/item_widget_timetable.xml index 899d75034..01f4525ec 100644 --- a/app/src/main/res/layout/item_widget_timetable.xml +++ b/app/src/main/res/layout/item_widget_timetable.xml @@ -1,129 +1,112 @@ - + android:textSize="22sp" + tools:text="1" + tools:textColor="?attr/colorTimetableChange" /> - - - + + android:textAppearance="?attr/textAppearanceBodySmall" + tools:text="08:00" /> + android:layout_marginTop="2dp" + android:textAppearance="?attr/textAppearanceBodySmall" + tools:text="09:45" /> + + + + - - + android:lines="1" + android:textSize="14sp" + tools:text="Programowanie aplikacji mobilnych i desktopowych" /> + + + + + + + + - + + + + + diff --git a/app/src/main/res/layout/item_widget_timetable_dark.xml b/app/src/main/res/layout/item_widget_timetable_dark.xml deleted file mode 100644 index 06233244b..000000000 --- a/app/src/main/res/layout/item_widget_timetable_dark.xml +++ /dev/null @@ -1,114 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/item_widget_timetable_empty.xml b/app/src/main/res/layout/item_widget_timetable_empty.xml new file mode 100644 index 000000000..a48b3645e --- /dev/null +++ b/app/src/main/res/layout/item_widget_timetable_empty.xml @@ -0,0 +1,36 @@ + + + + + + + + diff --git a/app/src/main/res/layout/item_widget_timetable_footer.xml b/app/src/main/res/layout/item_widget_timetable_footer.xml new file mode 100644 index 000000000..ef14da5d9 --- /dev/null +++ b/app/src/main/res/layout/item_widget_timetable_footer.xml @@ -0,0 +1,12 @@ + + diff --git a/app/src/main/res/layout/item_widget_timetable_small.xml b/app/src/main/res/layout/item_widget_timetable_small.xml deleted file mode 100644 index 1bf4072dc..000000000 --- a/app/src/main/res/layout/item_widget_timetable_small.xml +++ /dev/null @@ -1,98 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/item_widget_timetable_small_dark.xml b/app/src/main/res/layout/item_widget_timetable_small_dark.xml deleted file mode 100644 index 50bbbd031..000000000 --- a/app/src/main/res/layout/item_widget_timetable_small_dark.xml +++ /dev/null @@ -1,97 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/layout_preference_switch.xml b/app/src/main/res/layout/layout_preference_switch.xml new file mode 100644 index 000000000..c4f8a6c2c --- /dev/null +++ b/app/src/main/res/layout/layout_preference_switch.xml @@ -0,0 +1,5 @@ + + diff --git a/app/src/main/res/layout/subitem_dashboard_grades.xml b/app/src/main/res/layout/subitem_dashboard_grades.xml index 9354be3d6..c8165b954 100644 --- a/app/src/main/res/layout/subitem_dashboard_grades.xml +++ b/app/src/main/res/layout/subitem_dashboard_grades.xml @@ -23,7 +23,7 @@ android:id="@+id/dashboard_grades_subitem_grade_container" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginStart="8dp" + android:layout_marginStart="2dp" android:layout_marginBottom="6dp" android:orientation="horizontal" app:layout_constraintBottom_toBottomOf="parent" @@ -36,4 +36,4 @@ android:visibility="gone" tools:visibility="visible" /> - \ No newline at end of file + diff --git a/app/src/main/res/layout/subitem_dashboard_small_grade.xml b/app/src/main/res/layout/subitem_dashboard_small_grade.xml index 986d9602a..6800b72e9 100644 --- a/app/src/main/res/layout/subitem_dashboard_small_grade.xml +++ b/app/src/main/res/layout/subitem_dashboard_small_grade.xml @@ -5,7 +5,8 @@ android:layout_width="wrap_content" android:layout_height="20dp" android:layout_marginStart="4dp" - android:background="@color/grade_material_default" + android:background="@drawable/background_grade_small_rounded" + android:backgroundTint="@color/grade_material_default" android:gravity="center" android:maxLength="5" android:minWidth="20dp" diff --git a/app/src/main/res/layout/widget_luckynumber.xml b/app/src/main/res/layout/widget_luckynumber.xml index 360a19708..fc8acc605 100644 --- a/app/src/main/res/layout/widget_luckynumber.xml +++ b/app/src/main/res/layout/widget_luckynumber.xml @@ -1,63 +1,46 @@ - + android:adjustViewBounds="true" + android:importantForAccessibility="no" + android:scaleType="fitCenter" + android:src="@drawable/shape_badge" + android:tint="?attr/colorSurface" + app:tint="?attr/colorSurface" + tools:ignore="UseAppTint" /> - - + android:layout_gravity="center" + android:text="17" + android:textColor="?attr/colorPrimary" + android:textSize="72sp" + android:textStyle="bold" + tools:ignore="HardcodedText" /> - - + + diff --git a/app/src/main/res/layout/widget_luckynumber_dark.xml b/app/src/main/res/layout/widget_luckynumber_dark.xml deleted file mode 100644 index def110de2..000000000 --- a/app/src/main/res/layout/widget_luckynumber_dark.xml +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - - - - - diff --git a/app/src/main/res/layout/widget_timetable.xml b/app/src/main/res/layout/widget_timetable.xml index 059bb741f..b07cc78f6 100644 --- a/app/src/main/res/layout/widget_timetable.xml +++ b/app/src/main/res/layout/widget_timetable.xml @@ -1,99 +1,120 @@ - + android:backgroundTint="?attr/colorSecondaryContainer" + android:clipToOutline="true" + android:orientation="vertical" + android:paddingHorizontal="12dp" + android:theme="@style/Wulkanowy.Widget.Theme" + tools:context=".ui.modules.timetablewidget.TimetableWidgetProvider" + tools:targetApi="s"> - + android:layout_height="wrap_content" + android:gravity="center_vertical" + android:paddingTop="12dp" + android:paddingBottom="8dp"> - - - - - - - - + android:layout_width="48dp" + android:layout_height="48dp" + android:background="@drawable/background_timetable_widget_avatar" + android:backgroundTint="@android:color/transparent" + android:clickable="true" + android:clipToOutline="true" + android:contentDescription="@string/account_quick_manager" + android:focusable="true" + android:foreground="?attr/selectableItemBackgroundBorderless" + android:importantForAccessibility="yes" + android:outlineProvider="background" + tools:targetApi="s"> + + + + + + + + + android:tint="?attr/colorPrimary" + app:tint="?attr/colorPrimary" + tools:ignore="UseAppTint" /> - + android:tint="?attr/colorPrimary" + app:tint="?attr/colorPrimary" + tools:ignore="UseAppTint" /> - + + + android:layout_height="match_parent"> - - + + + + + + + diff --git a/app/src/main/res/layout/widget_timetable_dark.xml b/app/src/main/res/layout/widget_timetable_dark.xml deleted file mode 100644 index 9c8b8c561..000000000 --- a/app/src/main/res/layout/widget_timetable_dark.xml +++ /dev/null @@ -1,99 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/menu/action_menu_dashboard.xml b/app/src/main/res/menu/action_menu_dashboard.xml index 13565a196..71203d321 100644 --- a/app/src/main/res/menu/action_menu_dashboard.xml +++ b/app/src/main/res/menu/action_menu_dashboard.xml @@ -6,13 +6,13 @@ android:icon="@drawable/ic_settings_notifications" android:orderInCategory="1" android:title="@string/notifications_center_title" - app:iconTint="@color/material_on_surface_emphasis_medium" + app:iconTint="?colorControlNormal" app:showAsAction="ifRoom" /> - \ No newline at end of file + 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 5011e2356..57cf05ddb 100644 --- a/app/src/main/res/menu/action_menu_message_preview.xml +++ b/app/src/main/res/menu/action_menu_message_preview.xml @@ -8,20 +8,6 @@ android:title="@string/message_reply" app:iconTint="@color/material_on_surface_emphasis_medium" app:showAsAction="ifRoom" /> - - + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index d59ec8e17..a8699eec4 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -1,5 +1,6 @@ - + - \ No newline at end of file + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml deleted file mode 100644 index d59ec8e17..000000000 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png deleted file mode 100644 index 85f6a2c87..000000000 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png and /dev/null differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png deleted file mode 100644 index dc6ac682d..000000000 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png and /dev/null differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png deleted file mode 100644 index dd67b8771..000000000 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png and /dev/null differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png deleted file mode 100644 index b7d82f5d5..000000000 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png and /dev/null differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png deleted file mode 100644 index 7ad000eec..000000000 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png and /dev/null differ diff --git a/app/src/main/res/values-cs/preferences_values.xml b/app/src/main/res/values-cs/preferences_values.xml index 23073adf1..1590c47ab 100644 --- a/app/src/main/res/values-cs/preferences_values.xml +++ b/app/src/main/res/values-cs/preferences_values.xml @@ -51,6 +51,11 @@ Průměr z průměrů z obou semestrů Průměr známek z celého roku + + Nezobrazovat + Pouze mezi lekcemi + Před a mezi lekcemi + Šťastné číslo Nepřečtené zprávy diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index d036e7e42..e1cafa6ea 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -13,6 +13,7 @@ Prohlížeč protokolů Ladění Ladění oznámení + Vymazat soubory cookie webview Tvůrci Licence Zprávy @@ -26,6 +27,7 @@ Informace o žáku Domů Centrum oznámení + Konfigurace menu Semestr %1$d, %2$d/%3$d @@ -36,12 +38,14 @@ Přihlášení, číslo PESEL nebo e-mail Heslo Variace deníku UONET+ + Vlastní přípona domény Mobile API Scraper Hybridní Token PIN Symbol + Např. „lodz“ nebo „powiatjaroslawski“ Přihlásit Toto heslo je příliš krátké Přihlašovací údaje jsou nesprávné @@ -52,10 +56,12 @@ Neplatný e-mail Místo e-mailu použijte přiřazené přihlašovací údaje Použijte přiřazené přihlašovací nebo e-mail v @%1$s - Neplatný symbol + Invalid domain suffix + Neplatný symbol. Pokud jej nemůžete najít, kontaktujte školu + Nevymýšlejte si! Pokud symbol nemůžete najít, kontaktujte školu Žák nebyl nalezen. Zkontrolujte správnost symbolu a vybrané varianty deníku UONET+ 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+ + Symbol najdete na stránce deníku v  Uczeń→ Dostęp Mobilny → Wygeneruj kod dostępu.\n\nUjistěte se, že jste nastavili správnou variantu deníku v poli Variace deníku UONET+ na první přihlašovací obrazovce 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í @@ -67,16 +73,33 @@ Discord Poslat e-mail Ujistěte se, že jste vybrali správnou variantu deníku UONET+! - Zapomněl jsem své heslo + Obnovit heslo Obnovte svůj účet Obnovit Žák je už přihlášen Standardní + Jiná místa vyhledávání + Nebyli nalezeni žádní aktivní žáci + Zadejte jiný symbol + Získat pomoc + Celý název školy s městem (povinný) + Např. ZSTiO Jarosław nebo SP nr 99 w Łodzi + Zadejte správný název školy + Dodatečné informace v polštině (volitelné) + Např. „Ostatnio zmieniłem szkołę i…“ (Nedávno jsem změnil školu a…) nebo „Jestem rodzicem i nie widzę drugiego dziecka…“ (Jsem rodič a nevidím žádné další dítě…) + Odeslat + + Povolit oznámení + Povolit upozornění, abyste nezmeškali zprávu od učitele nebo o nové známce + Přeskočit + Zapnout Manažer účtů Přihlásit se Relace vypršela Relace vypršela. Přihlaste se prosím znovu + Heslo k vašemu účtu bylo změněno. Musíte se znovu přihlásit do Wulkanového + Heslo bylo změněno Podpora aplikace Líbí se Vám tato aplikace? Podpořte její vývoj tím, že povolíte neinvazivní reklamy, které můžete kdykoliv vypnout Zapnout reklamy @@ -95,6 +118,7 @@ Součet bodů Konečná známka Předpokládaná známka + Popisná známka Vypočítaný průměr Jak funguje vypočítaný průměr? Vypočítaný průměr je aritmetický průměr vypočítaný z průměrů předmětů. Umožňuje vám to znát přibližný konečný průměr. Vypočítává se způsobem zvoleným uživatelem v nastavení aplikaci. Doporučuje se vybrat příslušnou možnost. Důvodem je rozdílný výpočet školních průměrů. Pokud vaše škola navíc uvádí průměr předmětů na stránce deníku Vulcan, aplikace si je stáhne a tyto průměry nepočítá. To lze změnit vynucením výpočtu průměru v nastavení aplikaci.\n\nPrůměr známek pouze z vybraného semestru:\n1. Výpočet váženého průměru pro každý předmět v daném semestru\n2. Sčítání vypočítaných průměrů\n3. Výpočet aritmetického průměru součtených průměrů\n\nPrůměr průměrů z obou semestrů:\n1. Výpočet váženého průměru pro každý předmět v semestru 1 a 2\n2. Výpočet aritmetického průměru vypočítaných průměrů za semestry 1 a 2 pro každý předmět.\n3. Sčítání vypočítaných průměrů\n4. Výpočet aritmetického průměru sečtených průměrů\n\nPrůměr známek z celého roku:\n1. Výpočet váženého průměru za rok pro každý předmět. Konečný průměr v 1. semestru je nepodstatný.\n2. Sčítání vypočítaných průměrů\n3. Výpočet aritmetického průměru součtených průměrů @@ -138,6 +162,12 @@ Nové konečné známky Nové konečné známky + + Nová popisná známka + Nové popisné známky + Nové popisné známky + Nové popisné známky + Máte %1$d novou známku Máte %1$d nové známky @@ -156,6 +186,12 @@ Máte %1$d nových konečných známek Máte %1$d nových konečných známek + + Máte %1$d novou popisnou známku + Máte %1$d nové popisné známky + Máte %1$d nových popisných známek + Máte %1$d nových popisných známek + Lekce Učebna @@ -175,6 +211,12 @@ Změna učebny z %1$s na %2$s Změna učitele z %1$s na %2$s Změna předmětu z %1$s na %2$s + + Žádné lekce + Žádné lekce + Žádné lekce + Žádné lekce + Změna plánu lekcí Změny plánu lekcí @@ -309,9 +351,11 @@ Zpráva neexistuje Musíte vybrat alespoň 1 příjemce Obsah zprávy musí mít alespoň 3 znaky + Všechny poštovní schránky Pouze nepřečtené Pouze s přílohami Přečtena: %s + Přečtena přes: %1$d z %2$d osob %1$d zpráva %1$d zprávy @@ -339,6 +383,9 @@ %1$d vybraných Zprávy odstraněné + Vyberte poštovní schránku + Anonymní režim je zapnutý + Díky anonymnímu režimu není odesílatel upozorněn, když si zprávu přečtete Žádné informace o poznámkách Body @@ -482,6 +529,8 @@ Přítomnost na setkání Agenda + Místo + Téma Školní oznámení Žádná školní oznámení @@ -665,8 +714,10 @@ Vrátit Změnit Přidat do kalendáře + Zrušit Žádné lekce + Synchronizováno %1$s v %2$s Vybrat motiv Světlý Tmavý @@ -679,13 +730,15 @@ Zobrazit přítomnost Motiv Rozvíjení známek - Označit aktuální lekci Zobrazit skupiny vedle předmětů + Zobrazit prázdné dlaždice, kde není žádná lekce Zobrazit seznam grafů v známkách třídy Zobrazit předměty bez známek Známky barevné schéma Třídění předmětů Jazyk + Konfigurace menu + Nastavit pořadí funkcí v menu Oznámení Jiné Zobrazit oznámení @@ -719,10 +772,12 @@ Hodnota mínusu Odpovědět s historií zpráv Vypočítat aritmetický průměr, pokud žádná známka nemá váhu + Anonymní režim + Neinformovat o přečtení zprávy Podpora Ochrana osobních údajů Souhlasy - Souhlas se zpracováním údajů souvisejících s reklamami + Zobrazit souhlas se zpracováním údajů Zobrazit reklamy v aplikaci Podívejte se na jednu reklamu pro podporu projektu Souhlas se zpracováním dat @@ -731,13 +786,6 @@ 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í Oznámení @@ -787,6 +835,22 @@ Aktualizace byla stažena. Restartovat Aktualizace selhala! Wulkanowy nemusí fungovat správně. Zvažte aktualizaci + + Restartování aplikace + Pro uložení změn je nutné aplikaci restartovat + Restartovat + + Autorizace byla zamítnuta. Uvedené údaje se neshodují se záznamy v kanceláři tajemníka. + Neplatný PESEL + PESEL + Autorizovat + Autorizace byla úspěšně dokončena + Autorizace + Pro provoz aplikace potřebujeme potvrdit vaši identitu. Zadejte PESEL žáka <b>%1$s</b> v níže uvedeném poli + Zatím přeskočit + + Probíhá ověřování. Počkejte… + Úspěšně ověřeno Žádné internetové připojení Vyskytla se chyba. Zkontrolujte hodiny svého zařízení @@ -796,6 +860,7 @@ Probíhá údržba deníku UONET+. Zkuste to později znovu Neznámá chyba deniku UONET+. Prosím zkuste to znovu později Neznámá chyba aplikace. Prosím zkuste to znovu později + Vyžadováno ověření Captcha Vyskytla se neočekávaná chyba Funkce je deaktivována přes vaší školou Funkce není k dispozici. Přihlaste se v jiném režimu než Mobile API diff --git a/app/src/main/res/values-da-rDK/preferences_values.xml b/app/src/main/res/values-da-rDK/preferences_values.xml new file mode 100644 index 000000000..5aff12dec --- /dev/null +++ b/app/src/main/res/values-da-rDK/preferences_values.xml @@ -0,0 +1,70 @@ + + + + Light + Dark + Black (AMOLED) + + + System language + Polski + English + Pусский + Українська + Deutsch + Čeština + Slovenčina + + + 15 minutes + 30 minutes + 1 hour + 2 hours + 6 hours + 12 hours + 24 hours + + + 0,00 + 0,25 + 0,33 + 0,5 + 0,75 + + + Alphabetically + By date + By average + + + Dzienniczek+ + Wulkanowy + Grade colors in register + + + Up to 1 at once + Always expanded + Unlimited expansions + + + Average of grades only from selected semester + Average of averages from both semesters + Average of grades from the whole year + + + Don\'t show + Only between lessons + Before and between lessons + + + Lucky number + Unread messages + Attendance + Lessons + Grades + Homework + School announcements + Exams + Conferences + + diff --git a/app/src/main/res/values-de/preferences_values.xml b/app/src/main/res/values-de/preferences_values.xml index d9cac1959..d1001c74b 100644 --- a/app/src/main/res/values-de/preferences_values.xml +++ b/app/src/main/res/values-de/preferences_values.xml @@ -51,6 +51,11 @@ Durchschnittswert der Durchschnittswerte beider Semester Durchschnitt der Noten aus dem ganzen Jahr + + Don\'t show + Only between lessons + Before and between lessons + Glückszahl Ungelesene Nachrichten diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 4dfeee4f6..5bd71bb29 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -10,9 +10,10 @@ Einstellungen Mehr Über die Applikation - Log Viewer + Log viewer Debuggen Benachrichtigungen debuggen + Clear webview cookies Mitarbeiter Lizenzen Nachrichten @@ -26,6 +27,7 @@ Schülerinfo Übersicht Benachrichtigungszentrum + Menü Konfiguration Semester %1$d, %2$d/%3$d @@ -36,12 +38,14 @@ Anmeldung, PESEL oder e-mail Passwort UONET+ Registervariante + Custom domain suffix Mobile API Scraper Hybride Token PIN Symbol + E.g. \"lodz\" or \"powiatjaroslawski\" Anmelden Passwort ist zu kurz Anmeldedaten sind falsch @@ -52,10 +56,12 @@ Ungültige email Den zugewiesenen Login anstelle von email verwenden Benutze den zugewiesenen Login oder E-Mail in @%1$s - Ungültige symbol + Invalid domain suffix + Invalid symbol. If you cannot find it, please contact the school + Don\'t make this up! If you cannot find it, please contact the school Schüler nicht gefunden. Überprüfen Sie das Symbol und die gewählte Variation des UONET+ Registers 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 + Das Symbol kann auf der Registerseite in Student → Tost Möbeln → Registrieren Sie Ihr Mobilgerätgefunden 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 @@ -67,16 +73,33 @@ Discord email senden Stellen Sie sicher, dass Sie die richtige UONET+ Registervariation wählen! - Ich habe mein Passwort vergessen. + Reset password Ihr Konto wiederherstellen Wiederherstellen Student ist bereits angemeldet Standard + Andere Suchorte + Keine aktiven Schüler gefunden + Geben Sie ein anderes Symbol ein + Get help + Full school name with the town (required) + Np. ZSTiO Jarosław lub SP nr 99 w Łodzi + Enter correct name of the school + Additional information in Polish (optional) + Np. \"Ostatnio zmieniłem szkołę i…\" albo \"Jestem rodzicem i nie widzę drugiego dziecka…\" + Submit + + Benachrichtigungen aktivieren + Aktivieren Sie Benachrichtigungen, damit Sie keine Nachricht vom Lehrer oder eine neue Klasse verpassen + Überspringen + Ermöglichen Kundenbetreuer Anmelden Die Sitzung ist abgelaufen Die Sitzung ist abgelaufen, bitte loggen Sie sich erneut ein + Your account password has been changed. You need to log in to Wulkanowy again + Password changed Anwendungsunterstützung Gefällt Ihnen diese App? Unterstützen Sie ihre Entwicklung, indem Sie nicht-invasive Werbung aktivieren, die Sie jederzeit deaktivieren können Werbung aktivieren @@ -95,6 +118,7 @@ Gesamtpunkte Finaler Note Vorhergesagte Note + Descriptive grade Berechnender Durchschnitt Wie funktioniert der berechnete Durchschnitt? Der berechnete Mittelwert ist das arithmetische Mittel, das aus den Durchschnittswerten der Probanden errechnet wird. Es erlaubt Ihnen, den ungefähre endgültigen Durchschnitt zu kennen. Sie wird auf eine vom Anwender in den Anwendungseinstellungen gewählte Weise berechnet. Es wird empfohlen, die entsprechende Option zu wählen. Das liegt daran, dass die Berechnung der Schuldurchschnitte unterschiedlich ist. Wenn Ihre Schule den Durchschnitt der Fächer auf der Vulcan-Seite angibt, lädt die Anwendung diese Fächer herunter und berechnet nicht den Durchschnitt. Dies kann geändert werden, indem die Berechnung des Durchschnitts in den Anwendungseinstellungen erzwungen wird. \n\nDurchschnitt der Noten nur aus dem ausgewählten Semester :\n1. Berechnung des gewichteten Durchschnitts für jedes Fach in einem bestimmten Semester\n2. Addition der berechneten Durchschnittswerte\n3. Berechnung des arithmetischen Mittels der summierten Durchschnitte\nDurchschnitt der Durchschnitte aus beiden Semestern:\n1. Berechnung des gewichteten Durchschnitts für jedes Fach in Semester 1 und 2\n2. Berechnung des arithmetischen Mittels der berechneten Durchschnitte für Semester 1 und 2 für jedes Fach. \n3. Hinzufügen von berechneten Durchschnittswerten\n4. Berechnung des arithmetischen Mittels der summierten Durchschnitte\nDurchschnitt der Noten aus dem ganzen Jahr:\n1. Berechnung des gewichteten Jahresdurchschnitts für jedes Fach. Der Abschlussdurchschnitt im 1. Semester ist irrelevant. \n2. Addition der berechneten Durchschnittswerte\n3. Berechnung des arithmetischen Mittels der summierten Mittelwerte @@ -130,6 +154,10 @@ Neue Abschlussnote Neue Abschlussnoten + + New descriptive grade + New descriptive grades + Du hast %1$d Note bekommen Du hast %1$d Noten bekommen @@ -142,6 +170,10 @@ Sie haben %1$d Abschlussnote bekommen Sie haben %1$d Abschlussnoten bekommen + + You received %1$d descriptive grade + You received %1$d descriptive grades + Lektion Klassenzimmer @@ -161,6 +193,10 @@ Ä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 + + No lesson + No lessons + Änderung des Zeitplans Änderungen des Zeitplans @@ -275,9 +311,11 @@ Nachricht nicht vorhanden Sie müssen mindestens 1 Empfänger auswählen. Der Inhalt der Nachricht muss mindestens 3 Zeichen lang sein. + Alle postfächer Nur ungelesen Nur mit Anhängen Lesen: %s + Lesen von: %1$d von %2$d Personen %1$d Nachricht %1$d Nachrichten @@ -297,6 +335,9 @@ %1$d ausgewählt Nachrichten gelöscht + Postfach auswählen + Incognito mode is on + Thanks to incognito mode sender is not notified when you read the message Keine Informationen über Eintragen Punkte @@ -410,6 +451,8 @@ Teilnahme an einem Meeting Agenda + Ort + Thema Schulankündigungen Keine schulankündigungen @@ -577,8 +620,10 @@ lösen Ändern Zum Kalender hinzufügen + Stornieren Keine Lektionen + Synchronisiert am %1$s am %2$s Thema wählen Licht Dunkel @@ -591,13 +636,15 @@ Anwesendheit zeigen Thema Steigende Sorten - Aktuelle Lektion markieren Gruppen neben Schulfächen anzeigen + Show empty tiles where there\'s no lesson Liste der Diagramme in Klassenbewertungen anzeigen Schulfächer ohne Noten anzeigen Farbschema der Noten Schulfachen sortieren Sprache + Menü Konfiguration + Legen Sie die Reihenfolge der Funktionen im Menü fest Benachrichtigungen Sonstiges Benachrichtigungen anzeigen @@ -631,11 +678,13 @@ Wert des Minus Antwort mit Nachrichtenhistorie Arithmetisches Mittel anzeigen, wenn keine Gewichte angegeben sind + Incognito mode + Do not inform about reading the message Unterstützung - Privacy Policy - Agreements - Consent to processing of data related to ads - Show ads in app + Datenschutz-Bestimmungen + Vereinbarungen + Show consent to data processing + Anzeigen in der App anzeigen 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 @@ -643,13 +692,6 @@ 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 @@ -699,6 +741,22 @@ Ein Update wurde gerade heruntergeladen. Neustart Update fehlgeschlagen! Wulkanowy funktioniert möglicherweise nicht richtig. Überlegen Sie die Aktualisierung + + Neustart der Anwendung + Die Anwendung muss neu gestartet werden, damit die Änderungen gespeichert werden + Restart + + Authorization has been rejected. The data provided does not match the records in the secretary\'s office. + Invalid PESEL + PESEL + Authorize + Authorization completed successfully + Authorization + To operate the application, we need to confirm your identity. Please enter the student\'s PESEL <b>%1$s</b> in the field below + Skip for now + + Verification is in progress. Wait… + Verified successfully Keine Internetverbindung Es ist ein Fehler aufgetreten. Überprüfen Sie Ihre Geräteuhr @@ -708,6 +766,7 @@ Wartung im Gange UONET + Klassenbuch. Versuchen Sie es später noch einmal Unbekannter UONET + Registerfehler. Versuchen Sie es später erneut Unbekannter Anwendungsfehler. Bitte versuchen Sie es später noch einmal + Captcha verification required Ein unerwarteter Fehler ist aufgetreten Funktion, die von Ihrer Schule deaktiviert wurde Feature in diesem Modus nicht verfügbar diff --git a/app/src/main/res/values-es-rES/preferences_values.xml b/app/src/main/res/values-es-rES/preferences_values.xml new file mode 100644 index 000000000..5aff12dec --- /dev/null +++ b/app/src/main/res/values-es-rES/preferences_values.xml @@ -0,0 +1,70 @@ + + + + Light + Dark + Black (AMOLED) + + + System language + Polski + English + Pусский + Українська + Deutsch + Čeština + Slovenčina + + + 15 minutes + 30 minutes + 1 hour + 2 hours + 6 hours + 12 hours + 24 hours + + + 0,00 + 0,25 + 0,33 + 0,5 + 0,75 + + + Alphabetically + By date + By average + + + Dzienniczek+ + Wulkanowy + Grade colors in register + + + Up to 1 at once + Always expanded + Unlimited expansions + + + Average of grades only from selected semester + Average of averages from both semesters + Average of grades from the whole year + + + Don\'t show + Only between lessons + Before and between lessons + + + Lucky number + Unread messages + Attendance + Lessons + Grades + Homework + School announcements + Exams + Conferences + + diff --git a/app/src/main/res/values-it-rIT/preferences_values.xml b/app/src/main/res/values-it-rIT/preferences_values.xml new file mode 100644 index 000000000..5aff12dec --- /dev/null +++ b/app/src/main/res/values-it-rIT/preferences_values.xml @@ -0,0 +1,70 @@ + + + + Light + Dark + Black (AMOLED) + + + System language + Polski + English + Pусский + Українська + Deutsch + Čeština + Slovenčina + + + 15 minutes + 30 minutes + 1 hour + 2 hours + 6 hours + 12 hours + 24 hours + + + 0,00 + 0,25 + 0,33 + 0,5 + 0,75 + + + Alphabetically + By date + By average + + + Dzienniczek+ + Wulkanowy + Grade colors in register + + + Up to 1 at once + Always expanded + Unlimited expansions + + + Average of grades only from selected semester + Average of averages from both semesters + Average of grades from the whole year + + + Don\'t show + Only between lessons + Before and between lessons + + + Lucky number + Unread messages + Attendance + Lessons + Grades + Homework + School announcements + Exams + Conferences + + diff --git a/app/src/main/res/values-night-v31/styles.xml b/app/src/main/res/values-night-v31/styles.xml new file mode 100644 index 000000000..808bc714d --- /dev/null +++ b/app/src/main/res/values-night-v31/styles.xml @@ -0,0 +1,70 @@ + + + + + + + + diff --git a/app/src/main/res/values-night/styles.xml b/app/src/main/res/values-night/styles.xml index 881d5bd4f..5840a051e 100644 --- a/app/src/main/res/values-night/styles.xml +++ b/app/src/main/res/values-night/styles.xml @@ -1,20 +1,45 @@ - - - - - - + + diff --git a/app/src/main/res/values-pl/preferences_values.xml b/app/src/main/res/values-pl/preferences_values.xml index 456005747..2f2432e98 100644 --- a/app/src/main/res/values-pl/preferences_values.xml +++ b/app/src/main/res/values-pl/preferences_values.xml @@ -51,6 +51,11 @@ Średnia ze średnich z obu semestrów Średnia wszystkich ocen z całego roku + + Nie pokazuj + Tylko między lekcjami + Przed i między lekcjami + Szczęśliwy numerek Nieprzeczytane wiadomości diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index c9abbd022..70d4982b9 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -13,6 +13,7 @@ Przeglądarka logów Debugowanie Debugowanie powiadomień + Wyczyść ciasteczka webview Twórcy Licencje Wiadomości @@ -26,6 +27,7 @@ Informacje o uczniu Start Centrum powiadomień + Konfiguracja menu Semestr %1$d, %2$d/%3$d @@ -36,12 +38,14 @@ Login, PESEL lub adres e-mail Hasło Odmiana dziennika UONET+ + Niestandardowy sufiks domeny Mobilne API Scraper Hybrydowe Token PIN Symbol + Np. \"lodz\" czy \"powiatjaroslawski\" Zaloguj To hasło jest za krótkie Dane logowania są niepoprawne @@ -52,10 +56,12 @@ Nieprawidłowy adres e-mail Użyj loginu zamiast adresu e-mail Użyj loginu lub adresu e-mail w @%1$s - Nieprawidłowy symbol + Nieprawidłowy sufiks domeny + Nieprawidłowy symbol. Jeśli nie możesz go znaleźć, skontaktuj się ze szkołą + Nie zmyślaj! Jeśli nie możesz znaleźć symbolu, skontaktuj się ze szkołą Nie znaleziono ucznia. Sprawdź poprawność symbolu i wybranej odmiany dziennika UONET+ 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 + Symbol można znaleźć na stronie dziennika w Uczeń→ Dostęp Mobilny → Wygeneruj kod dostępu.\n\nUpewnij się, że ustawiłeś odpowiednią odmianę dziennika w polu Odmiana dziennika UONET+ na pierwszym ekranie logowania 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ń @@ -67,16 +73,33 @@ Discord Wyślij wiadomość e-mail Upewnij się, że została wybrana odpowiednia odmiana dziennika UONET+! - Nie pamiętam hasła + Zresetuj hasło Przywróć swoje konto Przywróć Uczeń jest już zalogowany Standardowa + Inne lokalizacje wyszukiwania + Nie znaleziono aktywnych uczniów + Wprowadź inny symbol + Uzyskaj pomoc + Pełna nazwa szkoły z miastem (wymagana) + Np. ZSTiO Jarosław lub SP nr 99 w Łodzi + Wprowadź poprawną nazwę szkoły + Dodatkowe informacje (po polsku) (nieobowiązkowo) + Np. \"Ostatnio zmieniłem szkołę i…\" albo \"Jestem rodzicem i nie widzę drugiego dziecka…\" + Wyślij + + Włącz powiadomienia + Włącz powiadomienia, aby nie przegapić wiadomości od nauczyciela lub nowej oceny + Pomiń + Włącz Menadżer kont Zaloguj się Sesja wygasła Sesja wygasła, zaloguj się ponownie + Hasło do Twojego konta zostało zmienione. Musisz zalogować się ponownie do Wulkanowego + Hasło zostało zmienione Wparcie aplikacji Podoba Ci się ta aplikacja? Wspieraj jej rozwój poprzez włączenie nieinwazyjnych reklam, które możesz wyłączyć w dowolnym momencie Włącz reklamy @@ -95,6 +118,7 @@ Suma punktów Ocena końcowa Przewidywana ocena + Ocena opisowa Obliczona średnia Jak działa obliczona średnia? Obliczona średnia jest średnią arytmetyczną obliczoną ze średnich przedmiotów. Pozwala ona na poznanie przybliżonej średniej końcowej. Jest obliczana w sposób wybrany przez użytkownika w ustawieniach aplikacji. Zaleca się wybranie odpowiedniej opcji. Dzieje się tak dlatego, że obliczanie średnich w szkołach różni się. Dodatkowo, jeśli twoja szkoła ma włączone średnie przedmiotów na stronie dziennika Vulcan, aplikacja pobiera je i ich nie oblicza. Można to zmienić, wymuszając obliczanie średniej w ustawieniach aplikacji.\n\nŚrednia ocen tylko z wybranego semestru:\n1. Obliczanie średniej arytmetycznej każdego przedmiotu w danym semestrze\n2. Zsumowanie obliczonych średnich\n3. Obliczanie średniej arytmetycznej zsumowanych średnich\n\nŚrednia ze średnich z obu semestrów:\n1.Obliczanie średniej arytmetycznej każdego przedmiotu w semestrze 1 i 2\n2. Obliczanie średniej arytmetycznej obliczonych średnich w semestrze 1 i 2 każdego przedmiotu.\n3. Zsumowanie obliczonych średnich\n4. Obliczanie średniej arytmetycznej zsumowanych średnich\n\nŚrednia wszystkich ocen z całego roku:\n1. Obliczanie średniej arytmetycznej z każdego przedmiotu w ciągu całego roku. Końcowa ocena w 1 semestrze jest bez znaczenia.\n2. Zsumowanie obliczonych średnich\n3. Obliczanie średniej arytmetycznej z zsumowanych średnich @@ -138,6 +162,12 @@ Nowe oceny końcowe Nowe oceny końcowe + + Nowa ocena opisowa + Nowe oceny opisowe + Nowe oceny opisowe + Nowe oceny opisowe + Masz %1$d nową ocenę Masz %1$d nowe oceny @@ -156,6 +186,12 @@ Masz %1$d nowych końcowych ocen Masz %1$d nowych końcowych ocen + + Masz %1$d nową ocenę opisową + Masz %1$d nowe oceny opisowe + Masz %1$d nowych ocen opisowych + Masz %1$d nowych ocen opisowych + Lekcja Sala @@ -175,6 +211,12 @@ Zmiana sali z %1$s na %2$s Zmiana nauczyciela z %1$s na %2$s Zmiana przedmiotu z %1$s na %2$s + + Brak lekcji + Brak lekcji + Brak lekcji + Brak lekcji + Zmiana planu lekcji Zmiany planu lekcji @@ -309,9 +351,11 @@ Wiadomość nie istnieje Musisz wybrać co najmniej 1 adresata Treść wiadomości musi zawierać co najmniej 3 znaki + Wszystkie skrzynki Tylko nieprzeczytane Tylko z załącznikami Przeczytana: %s + Przeczytana przez: %1$d z %2$d osób %1$d wiadomość %1$d wiadomości @@ -339,6 +383,9 @@ %1$d wybranych Wiadomości zostały usunięte + Wybierz skrzynkę + Tryb incognito jest włączony + Dzięki trybowi incognito nadawca nie zobaczy, że przeczytałeś tę wiadomość Brak informacji o uwagach Punkty @@ -482,6 +529,8 @@ Obecność na zebraniu Agenda + Miejsce + Temat Ogłoszenia szkolne Brak ogłoszeń szkolnych @@ -665,8 +714,10 @@ Cofnij Zmień Dodaj do kalendarza + Anuluj Brak lekcji + Zsynchronizowano %1$s o %2$s Wybierz motyw Jasny Ciemny @@ -679,13 +730,15 @@ Pokazuj obecność Motyw Rozwijanie ocen - Oznaczaj bieżącą lekcję Pokazuj grupę obok przedmiotu + Pokazuj puste kafelki gdzie nie ma lekcji Pokazuj listę wykresów w ocenach klasy Pokazuj przedmioty bez ocen Schemat kolorów ocen Sortowanie przedmiotów Język + Konfiguracja menu + Ustaw kolejność funkcji w menu Powiadomienia Inne Pokazuj powiadomienia @@ -719,10 +772,12 @@ Wartość minusa Odpowiadaj z historią wiadomości Licz średnią arytmetyczną, gdy żadna ocena nie ma wagi + Tryb incognito + Nie informuj o przeczytaniu wiadomości Wsparcie Polityka prywatności Zgody - Zgoda na przetwarzanie danych związanych z reklamami + Pokaż zgodę na przetwarzanie danych Pokazuj reklamy w aplikacji Obejrzyj pojedynczą reklamę, aby wesprzeć projekt Zgoda na przetwarzanie danych @@ -731,13 +786,6 @@ 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 @@ -787,6 +835,22 @@ Aktualizacja została pobrana. Uruchom ponownie Aktualizacja nie powiodła się! Wulkanowy może nie działać prawidłowo. Rozważ aktualizację + + Ponowne uruchomienie aplikacji + W celu zapisania zmian aplikacja musi zostać ponownie uruchomiona + Uruchom ponownie + + Autoryzacja została odrzucona. Podano dane niezgodne z danymi w sekretariacie. + Nieprawidłowy PESEL + PESEL + Potwierdź + Autoryzacja zakończona pomyślnie + Autoryzacja + Rodzicu, musimy mieć pewność, że Twój adres e-mail został powiązany z prawidłowym kontem ucznia. W celu autoryzacji konta podaj numer PESEL ucznia <b>%1$s</b> w polu poniżej + Na razie pomiń + + Trwa weryfikacja. Czekaj… + Pomyślnie zweryfikowano Brak połączenia z internetem Wystąpił błąd. Sprawdź poprawność daty w urządzeniu @@ -796,6 +860,7 @@ Trwa przerwa techniczna dziennika UONET+. Spróbuj ponownie później Nieznany błąd dziennika UONET+. Spróbuj ponownie później Nieznany błąd aplikacji. Spróbuj ponownie później + Wymagana weryfikacja captcha Wystąpił nieoczekiwany błąd Funkcja wyłączona przez szkołę Funkcja niedostępna. Zaloguj się w trybie innym niż Mobilne API diff --git a/app/src/main/res/values-ru/preferences_values.xml b/app/src/main/res/values-ru/preferences_values.xml index 8a8c260da..df3629c02 100644 --- a/app/src/main/res/values-ru/preferences_values.xml +++ b/app/src/main/res/values-ru/preferences_values.xml @@ -51,6 +51,11 @@ Средняя из средних оценок семестров Средняя из оценок со всего года + + Don\'t show + Only between lessons + Before and between lessons + Счастливый номер Непрочитанные письма diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index e1abb0293..717e02131 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -13,10 +13,11 @@ Просмотр журнала Отладка Отладка уведомлений + Clear webview cookies Разработчики Лицензии - Письма - Написать + Сообщения + Новое сообщение Новое домашнее задание Замечания и свершения Домашние задания @@ -26,6 +27,7 @@ Информация о ученике Главная Центр уведомлений + Настройка меню %1$d семестр, %2$d/%3$d @@ -36,12 +38,14 @@ Логин, PESEL или электронная почта Пароль Тип дневника UONET+ + Custom domain suffix Мобильный API Scraper Hybrid Token PIN Symbol + E.g. \"lodz\" or \"powiatjaroslawski\" Войти Пароль слишком короткий Данные для входа указаны неверно @@ -52,10 +56,12 @@ Неверный e-mail Используйте назначенный логин вместо e-mail Используйте назначенный логин или email в @%1$s - Неверный symbol + Invalid domain suffix + Invalid symbol. If you cannot find it, please contact the school + Don\'t make this up! If you cannot find it, please contact the school Ученик не найден. Проверьте symbol и выбранный тип дненика UONET+ Данный ученик уже авторизован - Symbol можно найти на странице регистрации в  Uczeń → Dostęp Mobilny → Zarejestruj urządzenie mobilne.\n\nУбедитесь, что вы выбрали соответствующий тип дневника в поле Тип дневника UONET+ на предыдущем экране + Symbol можно найти на странице регистрации в  Uczeń → Dostęp Mobilny → Wygeneruj kod dostępu.\n\nУбедитесь, что вы выбрали соответствующий тип дневника в поле Тип дневника UONET+ на первом экране входа Выберите учеников для авторизации в приложении Другие варианты В этом режиме не работают: счастливый номер, статистика класса по оценкам, статистика посещаемости и уроков, информация о школе и список зарегистрированных устройств @@ -67,19 +73,36 @@ Discord Отправить письмо Убедитесь, что вы выбрали правильный тип дневника UONET+ - Забыли пароль? + Reset password Восстановите свой аккаунт Восстановить Ученик уже авторизован Стандартный + Другие места поиска + Не найдено активных учеников + Введите другой symbol + Get help + Full school name with the town (required) + Np. ZSTiO Jarosław lub SP nr 99 w Łodzi + Enter correct name of the school + Additional information in Polish (optional) + Np. \"Ostatnio zmieniłem szkołę i…\" albo \"Jestem rodzicem i nie widzę drugiego dziecka…\" + Submit + + Включить уведомления + Включить уведомления, чтобы вы не пропустили сообщение от учителя или новую оценку + Пропустить + Включить Менеджер аккаунтов Войти Сеанс истёк Сеанс истёк, авторизуйтесь снова - Application support - Do you like this app? Support its development by enabling non-invasive ads that you can disable at any time - Enable ads + Your account password has been changed. You need to log in to Wulkanowy again + Password changed + Поддержка приложения + Вам нравится это приложение? Поддержите его разработку, включив неинвазивную рекламу, которую можно отключить в любое время + Включить рекламу Оценка %d семестр @@ -95,9 +118,10 @@ Сумма баллов Итоговая оценка Ожидаемая оценка + Descriptive grade Рассчитанная средняя оценка Как работает \"Рассчитанная средняя оценка\"? - 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 + Рассчитанная средняя оценка - это среднее арифметическое, рассчитанное на основе средних оценок по предметам. Это позволяет узнать приблизительную итоговую среднюю оценку. Она рассчитывается способом, выбранным пользователем в настройках приложения. Рекомендуется выбрать подходящий вариант, так как каждая школа по разному считает среднюю оценку. Кроме того, если ваша школа выставляет средние оценки по предметам на странице Vulcan, приложение просто загрузит их. Это можно изменить, заставив приложение считать среднюю оценку в настройках.\n\nСредняя из оценок выбранного семестра:\n1. Вычисление средневзвешенного значения по каждому предмету за семестр\n2.Суммирование вычисленных значений\n3. Вычисление среднего арифметического суммированных значений\n\nСредняя из средних оценок семестров:\n1.Расчет средневзвешенного значения для каждого предмета в семестрах. \n2. Вычисление среднего арифметического из средневзвешенных значений для каждого предмета в семестрах.\n3. Суммирование средних арифметических\n4. Вычисление среднего арифматического из суммированных значений\n\nСредняя из оценок со всего года:\n1. Расчет средневзвешенного значения по каждому предмету за год. Итоговое среднее значение за 1 семестр не имеет значения.\n2. Суммирование вычисленных средних\n3. Расчет среднего арифметического суммированных чисел Как работает \"Итоговая средняя оценка\"? Итоговая средняя оценка - это среднее арифметическое, рассчитанное из всех имеющихся на данный момент итоговых оценок в семестре.\n\nРассчет происходит следующим образом:\n1. Суммирование итоговых оценок, выставленных преподавателями\n2. Полученная сумма делится на число предметов, по которым выставлены оценки Итоговая средняя оценка @@ -138,6 +162,12 @@ Новые итоговые оценки Новые итоговые оценки + + New descriptive grade + New descriptive grades + New descriptive grades + New descriptive grades + Вы получили %1$d новую оценку Вы получили %1$d новые оценки @@ -156,6 +186,12 @@ Вы получили %1$d новых итоговых оценок Вы получили %1$d новых итоговые оценки + + You received %1$d descriptive grade + You received %1$d descriptive grades + You received %1$d descriptive grades + You received %1$d descriptive grades + Урок Аудитория @@ -175,6 +211,12 @@ Аудитория изменена с %1$s на %2$s Учитель изменён с %1$s на %2$s Тема изменена с %1$s на %2$s + + No lesson + No lessons + No lessons + No lessons + Изменение расписания Изменения расписания @@ -286,7 +328,7 @@ Отправленные Корзина (нет темы) - Нет писем + Нет сообщений От: Кому: Дата: %1$s @@ -296,22 +338,24 @@ Отменить выбор Перенести в корзину Удалить навсегда - Письмо успешно удалено - student - parent - guardian - employee + Сообщение успешно удалено + ученик + родитель + опекун + работник Поделиться Печать Тема Содержание - Письмо успешно отправлено - Письма не существует + Сообщение успешно отправлено + Сообщения не существует Вы должны выбрать как минимум одного получателя Текст сообщения должен содержать как минимум 3 знака + Все почтовые ящики Только непрочитанные Только с вложениями Прочитано: %s + Прочитано: %1$d из %2$d человек %1$d сообщение %1$d сообщения @@ -324,8 +368,8 @@ Новые сообщения Новые сообщения - Вы хотите восстановить черновик письма? - Вы хотите восстановить черновик письма с получателями: %s? + Вы хотите восстановить черновик сообщения? + Вы хотите восстановить черновик сообщения с получателями: %s? Вы получили %1$d новое сообщение Вы получили %1$d новых сообщения @@ -339,6 +383,9 @@ %1$d выбрано Сообщение удалено + Выбрать почтовый ящик + Incognito mode is on + Thanks to incognito mode sender is not notified when you read the message Нет записей о замечаниях и свершениях Баллы @@ -482,6 +529,8 @@ Присутствует на встрече Повестка дня + Место + Тема Объявления школы Нет школьных объявлений @@ -665,8 +714,10 @@ Отменить Изменить Добавить в календарь + Отменить Нет уроков + Синхронизировано %1$s в %2$s Выбрать тему Светлая Тёмная @@ -679,13 +730,15 @@ Показывать присутствия Тема Разворачивание оценок - Отметить текущий урок Показать группы рядом с темами + Show empty tiles where there\'s no lesson Показывать диаграммы в оценках класса Показать предметы без оценок Цветовая схема оценок Сортировка уроков Язык + Настройка меню + Установить порядок функций в меню Уведомления Прочее Показывать уведомления @@ -719,11 +772,13 @@ Стоимость минуса Отвечать с историей сообщений Показывать среднее арифметическое при отсутствии стоимости + Incognito mode + Do not inform about reading the message Поддержка - Privacy Policy - Agreements - Consent to processing of data related to ads - Show ads in app + Политика приватности + Соглашения + Show consent to data processing + Показать рекламу в приложении Посмотреть рекламу для поддержки проекта Согласие на обработку данных Для просмотра рекламы вы должны согласиться с условиями обработки данных нашей Политики конфиденциальности @@ -731,13 +786,6 @@ Политика конфиденциальности Реклама загружается Спасибо за вашу поддержку, возвращайтесь позже для дополнительной рекламы - 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 Расширенные Внешний вид и поведение Уведомления @@ -760,14 +808,14 @@ Значения плюса и минуса, расчёт средней оценки Расширенные Версия приложения, разработчики, соц. сети - Посмотреть рекламу, чтобы поддержать преокт + Посмотреть рекламу, чтобы поддержать проект Новые оценки Новое домашнее задание Новые встречи Новые тесты Счастливый номер - Новые письма + Новые сообщения Новые замечания Новые школьные объявления Push-уведомления @@ -787,6 +835,22 @@ Только что было скачано обновление. Перезапустить Не удалось обновить! Wulkanowy может работать некорректно. Рассмотрите возможность обновления + + Перезапуск приложение + Для сохранения изменений необходимо перезапустить приложение + Перезапустить + + Авторизация отклонена. Предоставленные данные не соответствуют записям в кабинете секретаря. + Неправильный номер PESEL + Номер PESEL + Авторизовать + Авторизация прошла успешно + Авторизация + Для работы приложения нам необходимо подтвердить вашу личность. Введите PESEL учащегося <b>%1$s</b> в поле ниже + Пропустить сейчас + + Verification is in progress. Wait… + Verified successfully Интернет-соединение отсутствует Произошла ошибка. Проверьте время на вашем устройстве @@ -796,6 +860,7 @@ UONET+ проводит техническое обслуживание, повторите попытку позже Неизвестная ошибка дневника UONET+, повторите попытку позже Неизвестная ошибка приложения, повторите попытку позже + Captcha verification required Произошла непредвиденная ошибка Функция отключена вашей школой Функция недоступна в режиме 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 e4331315a..6cd221540 100644 --- a/app/src/main/res/values-sk/preferences_values.xml +++ b/app/src/main/res/values-sk/preferences_values.xml @@ -51,6 +51,11 @@ Priemer z priemerov z oboch semestrov Priemer známok z celého roka + + Nezobrazovať + Iba medzi lekciami + Pred a medzi lekciami + Šťastné číslo Neprečítané správy diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 1c6eae8c0..368ead9d5 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -13,6 +13,7 @@ Prehliadač protokolov Ladenie Ladenie oznámení + Vymazať súbory cookie webview Tvorcovia Licencie Správy @@ -26,6 +27,7 @@ Informácie o žiakovi Domov Centrum oznámení + Konfigurácia menu Semester %1$d, %2$d/%3$d @@ -35,13 +37,15 @@ Email Prihlásenie, číslo PESEL alebo e-mail Heslo - Variácie denníka UONET+ + Variácia denníka UONET+ + Vlastná prípona domény Mobile API Scraper Hybridné Token PIN Symbol + Napr. „lodz“ alebo „powiatjaroslawski“ Prihlásiť Toto heslo je príliš krátke Prihlasovacie údaje sú nesprávne @@ -52,10 +56,12 @@ Neplatný e-mail Namiesto e-mailu použite priradené prihlasovacie údaje Použite priradené prihlasovacie alebo e-mail v @%1$s - Neplatný symbol + Invalid domain suffix + Neplatný symbol. Pokiaľ ho nemôžete nájsť, kontaktujte školu + Nevymýšľajte si! Pokiaľ symbol nemôžete nájsť, kontaktujte školu Žiak nebol nájdený. Skontrolujte správnosť symbolu a vybrané varianty denníka UONET+ 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+ + Symbol nájdete na stránke denníka v  Uczeń→ Dostęp Mobilny → Wygeneruj kod dostępu.\n\nUistite sa, že ste nastavili správny variant denníka v poli Variácia denníka UONET+ na prvej prihlasovacej obrazovke 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í @@ -67,16 +73,33 @@ Discord Poslať e-mail Uistite sa, že ste vybrali správny variant denníka UONET+! - Zabudol som heslo + Obnoviť heslo Obnovte svoj účet Obnoviť Žiak je už prihlásený Štandardná + Iné miesta vyhľadávania + Neboli nájdení žiadni aktívni žiaci + Zadajte iný symbol + Získať pomoc + Celý názov školy s mestom (povinný) + Napr. ZSTiO Jarosław alebo SP nr 99 w Łodzi + Zadajte správny názov školy + Dodatočné informácie v poľštine (voliteľné) + Napr. „Ostatnio zmieniłem szkołę i…“ (Nedávno som zmenil školu a…) alebo „Jestem rodzicem i nie widzę drugiego dziecka…“ (Som rodič a nevidím žiadne ďalšie dieťa…) + Odoslať + + Povoliť oznámenia + Povoliť oznámenia, aby ste nezmeškali správu od učiteľa alebo o novej známke + Preskočiť + Zapnúť Manažér účtov Prihlásiť sa Relácia vypršala Relácia vypršala. Prihláste sa prosím znovu + Heslo k vášmu účtu bolo zmenené. Musíte sa znovu prihlásiť do Wulkanového + Heslo bolo zmenené Podpora aplikácie Páči sa Vám táto aplikácia? Podporte jej vývoj tým, že povolíte neinvazívne reklamy, ktoré môžete kedykoľvek vypnúť Zapnúť reklamy @@ -95,6 +118,7 @@ Súčet bodov Konečná známka Predpokladaná známka + Popisná známka Vypočítaný priemer Ako funguje vypočítaný priemer? Vypočítaný priemer je aritmetický priemer vypočítaný z priemerov predmetov. Umožňuje vám to poznať približný konečný priemer. Vypočítava sa spôsobom zvoleným užívateľom v nastaveniach aplikácii. Odporúča sa vybrať príslušnú možnosť. Dôvodom je rozdielny výpočet školských priemerov. Ak vaša škola navyše uvádza priemer predmetov na stránke denníka Vulcan, aplikácia si ich stiahne a tieto priemery nepočíta. To možno zmeniť vynútením výpočtu priemeru v nastavení aplikácii.\n\nPriemer známok iba z vybraného semestra:\n1. Výpočet váženého priemeru pre každý predmet v danom semestri\n2. Sčítanie vypočítaných priemerov\n3. Výpočet aritmetického priemeru součtených priemerov\n\nPriemer priemerov z oboch semestrov:\n1. Výpočet váženého priemeru pre každý predmet v semestri 1 a 2\n2. Výpočet aritmetického priemeru vypočítaných priemerov za semestre 1 a 2 pre každý predmet.\n3. Sčítanie vypočítaných priemerov\n4. Výpočet aritmetického priemeru součtených priemerov\n\nPriemer známok z celého roka:\n1. Výpočet váženého priemeru za rok pre každý predmet. Konečný priemer v 1. semestri je nepodstatný.\n2. Sčítanie vypočítaných priemerov\n3. Výpočet aritmetického priemeru součtených priemerov @@ -138,6 +162,12 @@ Nové konečné známky Nové konečné známky + + Nová popisná známka + Nové popisné známky + Nové popisné známky + Nové popisné známky + Máte %1$d novú známku Máte %1$d nové známky @@ -156,6 +186,12 @@ Máte %1$d nových konečných známok Máte %1$d nových konečných známok + + Máte %1$d novú popisnú známku + Máte %1$d nové popisné známky + Máte %1$d nových popisných známok + Máte %1$d nových popisných známok + Lekcia Učebňa @@ -175,6 +211,12 @@ Zmena učebne z %1$s na %2$s Zmena učiteľa z %1$s na %2$s Zmena predmetu z %1$s na %2$s + + Žiadne lekcie + Žiadne lekcie + Žiadne lekcie + Žiadne lekcie + Zmena plánu lekcií Zmeny plánu lekcií @@ -309,9 +351,11 @@ Správa neexistuje Musíte vybrať aspoň 1 príjemca Obsah správy musí mať aspoň 3 znaky + Všetky poštové schránky Iba neprečítané Iba s prílohami Prečítaná: %s + Prečítaná cez: %1$d z %2$d osôb %1$d správa %1$d správy @@ -339,6 +383,9 @@ %1$d vybraných Správy odstránené + Vyberte poštovú schránku + Režim inkognito je zapnutý + Vďaka inkognito režimu nie je odosielateľ upozornený, keď si správu prečítate Žiadne informácie o poznámkach Body @@ -482,6 +529,8 @@ Prítomnosť na stretnutí Agenda + Miesto + Téma Školské oznámenia Žiadne školské oznámenia @@ -665,8 +714,10 @@ Vrátiť Zmeniť Pridať do kalendára + Zrušiť Žiadne lekcie + Synchronizované %1$s v %2$s Vybrať motív Svetlý Tmavý @@ -679,13 +730,15 @@ Zobraziť prítomnosť Motív Rozvijanie známok - Označiť aktuálne lekciu Zobraziť skupiny vedľa predmetov + Zobraziť prázdne dlaždice, kde nie je žiadne lekcie Zobraziť zoznam grafov v známkach triedy Zobraziť predmety bez známok Známky farebnú schému Triedenie predmetov Jazyk + Konfigurácia menu + Nastaviť poradie funkcií v menu Oznámenia Iné Zobraziť oznámenia @@ -719,10 +772,12 @@ Hodnota mínusu Odpovedať s históriou správ Vypočítať aritmetický priemer, ak žiadna známka nemá váhu + Režim inkognito + Neinformovať o prečítaní správy Podpora Ochrana osobných údajov Súhlasy - Súhlas so spracovaním údajov súvisiacich s reklamami + Zobraziť súhlas so spracovaním údajov Zobraziť reklamy v aplikácii Pozrite sa na jednu reklamu pre podporu projektu Súhlas so spracovaním dát @@ -731,13 +786,6 @@ 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 Oznámenia @@ -787,6 +835,22 @@ Aktualizácia bola stiahnutá. Reštartovať Aktualizácia zlyhala! Wulkanowy nemusí fungovať správne. Zvážte aktualizáciu + + Reštartovanie aplikácie + Pre uloženie zmien je nutné aplikáciu reštartovať + Reštartovať + + Autorizácia bola zamietnutá. Uvedené údaje sa nezhodujú so záznamami v kancelárii tajomníka. + Neplatný PESEL + PESEL + Autorizovať + Autorizácia bola úspešne dokončená + Autorizácia + Na prevádzku aplikácie potrebujeme potvrdiť vašu identitu. Zadajte PESEL žiaka <b>%1$s</b> v nižšie uvedenom poli + Zatiaľ preskočiť + + Overovanie prebieha. Počkajte… + Úspešne overené Žiadne internetové pripojenie Vyskytla sa chyba. Skontrolujte hodiny svojho zariadenia @@ -796,6 +860,7 @@ Prebieha údržba denníka UONET+. Skúste to neskôr znova Neznáma chyba dennika UONET+. Prosím skúste to znova neskôr Neznáma chyba aplikácie. Prosím skúste to znova neskôr + Vyžaduje sa overenie Captcha Vyskytla sa neočakávaná chyba Funkcia je deaktivovaná cez vašou školou Funkcia nie je k dispozícii. Prihláste sa v inom režime než Mobile API diff --git a/app/src/main/res/values-uk/preferences_values.xml b/app/src/main/res/values-uk/preferences_values.xml index 44acd18e9..c02efb54a 100644 --- a/app/src/main/res/values-uk/preferences_values.xml +++ b/app/src/main/res/values-uk/preferences_values.xml @@ -51,6 +51,11 @@ Середнє значення з обох семестрів Середня оцінка з цілого року + + Не показувати + Тільки між уроками + Перед і між уроками + Щасливий номер Непрочитані листи diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index f90e78e7d..3d10f1179 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -13,6 +13,7 @@ Переглядач логів Відладка Відладка сповіщень + Очистити кукі веб - перегляду Розробники Ліцензії Листи @@ -26,6 +27,7 @@ Інформація про учня Головна Центр сповіщень + Конфігурація меню %1$d семестр, %2$d/%3$d @@ -36,12 +38,14 @@ Логін, PESEL або e-mail Пароль Тип щоденника UONET+ + Користувацький суфікс домену Мobile API Scraper Hybrid Token PIN Symbol + Напр. \"lodz\" чи \"powiatjaroslawski\" Увійти Занадто короткий пароль Вказані невірні дані @@ -52,10 +56,12 @@ Недійсна адреса e-mail Використовуйте призначений логін замість адреси e-mail Використовуйте призначений логін або адресу e-mail в @%1$s - Неправильний symbol - Студента не знайдено. Перевірте symbol та обранний тип щоденника UONET+ + Invalid domain suffix + Некоректний символ. Якщо ви не можете знайти його, будь ласка, зв\'яжіться зі школою + Не вигадуйте! Якщо ви не можете знайти його, будь ласка, зв\'яжіться зі школою + Студента не знайдено. Перевірте symbol та обраний тип щоденника UONET+ Цього учня вже авторизовано - Symbol можна знайти на сторінці реєстрації в  Uczeń→ Dostęp Mobilny → Zarejestruj urządzenie mobilne.\n\nПереконайтесь, що ви встановили відповідний тип щоденника в полі Тип щоденника UONET+ на попередньому екрані + Symbol можно знайти на сторінці щоденника у Uczeń → Dostęp Mobilny → Wygeneruj kod dostępu.\n\nПереконайтеся, що ви вказали відповідний щоденник у полі Тип щоденника UONET+ на першому екрані логування Виберіть учнів для авторизації в додатку Інші варіанти У цьому режимі не працюють: щасливий номер, статистика класу по оцінкам, статистика відвідуваності та уроків, інформація про школу та список зареєстрованих пристроїв @@ -67,19 +73,36 @@ Discord Надіслати електронний лист Переконайтеся, що ви вибрали правильний тип щоденника UONET+! - Забули пароль? + Скинути пароль Відновіть свій обліковий запис Відновити Учня вже авторизовано Стандартний + Інші розташування пошуку + Активних учнів не знайдено + Введіть інший symbol + Отримати допомогу + Повна назва школи з містом (обов\'язково) + Напр. ZSTiO Jarosław lub SP nr 99 w Łodzi + Введіть правильну назву школи + Додаткова інформація польською мовою (за бажанням) + Напр. \"Я нещодавно змінив школу і...\" або \"Я батько і не бачу другу дитину...\" + Надіслати + + Увімкнути сповіщення + Увімкнути сповіщення, щоб не пропустити лист від вчителя або нову оцінку + Пропустити + Увімкнути Змінити облікові записи Увійти Минув термін дії сесії Минув термін дії сесії, авторизуйтеся знову - Application support - Do you like this app? Support its development by enabling non-invasive ads that you can disable at any time - Enable ads + Пароль вашого облікового запису був змінений. Ви повинні увійти в Wulkanowy знову + Пароль змінено + Підтримка додатку + Вам подобається цей додаток? Підтримайте його розвиток, увімкнувши неінвазивну рекламу, яку ви можете відключити в будь-який час + Увімкнути рекламу Оцінка %d семестр @@ -95,9 +118,10 @@ Всього балів Підсумкова оцінка Передбачувана оцінка + Описова оцінка Розрахована середня оцінка Як працює \"Розрахована середня оцінка\"? - 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 + Розрахована середня оцінка - це середнє арифметичне, обчислене з середніх оцінок з предметів. Це дозволяє дізнатися приблизну кінцеву середню оцінку. Вона розраховується спосібом, обраним користувачем у налаштуваннях програми. Рекомендується вибрати відповідний варіант, тому що кожна школа по різному розраховує середню оцінку. Крім того, якщо у вашій школі повідомляється середня оцінка з предметів на сторінці Vulcan, програма тільки завантажує ці оцінки і не розраховує їх самостійно. Це можна змінити шляхом примусового розрахунку середньоЇ оцінки в налаштуваннях програми.\n\nСередні оцінки тільки за обраний семестр:\n1. Розрахунок середньозваженого числа для кожного предмета в даному семестрі\n2. Сумування розрахованих числ\n3. Розрахунок середнього арифметичного з сумованих чисел\n\nСереднє значення з обох семестрів:\n1. Обчислення середньозваженого числа для кожного предмета у 1 та 2 семестрі\n2. Обчислення середнього арифметичного з розрахованих середньозважених числ за 1 та 2 семестри для кожного предмета.\n3. Додавання розрахованих середніх\n4. Розрахунок середнього арифметичного підсумованих середніх значень\n\nСереднє значення оцінок за весь рік: \n1. Розрахунок середньозваженого числа за рік для кожного предмета. Підсумковий середній показник у 1-му семестрі не має значення.\n2. Сумування розрахованих середніх\n3. Обчислення середнього арифметичного з суммованих середніх Як працює \"Підсумкова середня оцінка\"? Підсумкове середнє значення - це середнє арифметичне, обчислене з усіх наявних наразі підсумкових оцінок у даному семестрі. \n\nСхема обчислення складається з таких кроків:\n1. Сумування підсумкових оцінок, виставленних викладачами\n2. Ділення на кількість предметів, з яких виставлені ці оцінки Підсумкова середня оцінка @@ -138,6 +162,12 @@ Нові підсумкові оцінки Нові підсумкові оцінки + + Нова описова оцінка + Нових описових оцінок + Описових оцінок + Нові описові оцінки + Ви отримали %1$d нову оцінку Ви отримали %1$d нові оцінки @@ -156,6 +186,12 @@ Ви отримали %1$d нових підсумкових оцінок Ви отримали %1$d нових підсумкових оцінок + + Ви отримали %1$d описову оцінку + Ви отримали %1$d нові описові оцінки + Ви отримали %1$d нових описових оцінок + Ви отримали %1$d нових описових оцінок + Урок Аудиторія @@ -175,6 +211,12 @@ Зміна аудіторії з %1$s на %2$s Зміна вчителя з %1$s на %2$s Зміна теми з %1$s на %2$s + + Немає уроку + Немає уроків + Немає уроків + Немає уроків + Зміна у розкладі Зміни у розкладі @@ -220,7 +262,7 @@ Всі в серії Час початку Час завершення - Час завершення має бути більшим, ніж час початку + Час завершення має бути пізніше часу початку Підсумок відвідуваності Відсутність зі шкільних причин @@ -297,10 +339,10 @@ Перемістити до кошика Видалити назавжди Лист було успішно видалено - student - parent - guardian - employee + учень + родич + опікун + працівник Поділитись Друк Тема @@ -309,9 +351,11 @@ Такого листа не існує Необхідно обрати принаймні 1 адресата Зміст листа повинен складатися принаймні з 3 знаків + Усі поштові скриньки Лише непрочитані Тільки з вкладеннями Прочитаний: %s + Прочитано: %1$d з %2$d осіб %1$d лист %1$d листи @@ -338,7 +382,10 @@ %1$d вибрано %1$d вибрано - Листи видалені + Листи видалено + Вибрати поштову скриньку + Режим анонімності включено + Завдяки режиму анонімності, відправник не буде сповіщений коли ви прочитаєте повідомлення Немає інформації о зауваженнях Бали @@ -482,6 +529,8 @@ Присутність на зустрічі Порядок денний + Місце + Тема Оголошення школи Немає шкільних оголошень @@ -509,7 +558,7 @@ Ви впевнені, що хочете вийти з цього облікового запису? Вийти з облікового запису учня Обліковий запис учня - Обліковий запис батька + Обліковий запис родителя Змінити дані Змінити облікові записи Вибрати учня @@ -665,8 +714,10 @@ Відмінити Змінити Додати у календар + Скасувати Немаэ уроків + Синхронізовано %1$s в %2$s Увібрати тему Яскрава Темна @@ -679,13 +730,15 @@ Показувати присутність Тема Розгортання оцінок - Позначити поточний урок Показувати групи поруч з темами + Показувати порожні плитки там, де немає уроків Показувати діаграми в оцінках класу Показати предмети без оцінок Схема кольорів оцінок Сортування предметів Мова + Конфігурація меню + Встановити порядок функцій в меню Повідомлення Інше Показувати повідомлення @@ -719,11 +772,13 @@ Вартість мінуса Відповісти з історією повідомлень Вилічити середню аритметичну, якщо оцінка немає вартості + Анонімний режим + Не повідомляти про прочитання повідомлення Підтримка - Privacy Policy - Agreements - Consent to processing of data related to ads - Show ads in app + Політика конфіденційності + Угоди + Показати згоду на обробку даних + Показувати рекламу в додатку Подивіться одну рекламу для підтримки проєкту Згода в обробці даних Щоб переглянути рекламу, ви повинні погодитися з умовами обробки даних нашої Політики конфіденційності @@ -731,13 +786,6 @@ Політика конфіденційності Реклама завантажується Дякуємо за вашу підтримку, повертайтеся пізніше для більшої кількості реклам - 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 Додатково Вигляд та поведінка Сповіщення @@ -787,6 +835,22 @@ Щойно завантажено оновлення. Перезавантажити Помилка оновлення! Wulkanowy може не працювати належним чином. Подумайте про оновлення + + Перезавантаження додатку + Додаток потрібно перезавантажити для збереження змін + Перезавантажити + + Авторизацію відхилено. Надані дані не збігаються із записами в кабінеті секретаря. + Неправильний PESEL + Число PESEL + Авторизовать + Авторизація пройшла успішно + Авторизувати + Для роботи програми нам потрібно підтвердити вашу особу. Будь ласка, введіть число PESEL <b>%1$s</b> студента в поле нижче + Поки що пропустити + + Верифікація в процесі. Чекайте… + Верифікація завершена Немає з\'єднання з інтернетом Сталася помилка. Перевірте годинник пристрою @@ -796,6 +860,7 @@ UONET+ проводить технічне осблуговування, спробуйте пізніше Невідома помилка щоденника UONET+, спробуйте пізніше Невідома помилка програми, спробуйте пізніше + Необхідна перевірка Captcha Відбулася несподівана помилка Функція вимкнена вашою школою Функція недоступна в режимі Mobile API. Увійдіть в інший режим diff --git a/app/src/main/res/values-v23/styles.xml b/app/src/main/res/values-v23/styles.xml index 840f53573..95450ae1a 100644 --- a/app/src/main/res/values-v23/styles.xml +++ b/app/src/main/res/values-v23/styles.xml @@ -3,6 +3,11 @@ - \ No newline at end of file + + + diff --git a/app/src/main/res/values-v26/styles.xml b/app/src/main/res/values-v26/styles.xml deleted file mode 100644 index 3fb0a5dd8..000000000 --- a/app/src/main/res/values-v26/styles.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - diff --git a/app/src/main/res/values-v27/styles.xml b/app/src/main/res/values-v27/styles.xml new file mode 100644 index 000000000..1cbe97911 --- /dev/null +++ b/app/src/main/res/values-v27/styles.xml @@ -0,0 +1,16 @@ + + + + + + diff --git a/app/src/main/res/values-v28/styles.xml b/app/src/main/res/values-v28/styles.xml deleted file mode 100644 index a936566f0..000000000 --- a/app/src/main/res/values-v28/styles.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/values-v29/styles.xml b/app/src/main/res/values-v29/styles.xml deleted file mode 100644 index a936566f0..000000000 --- a/app/src/main/res/values-v29/styles.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/values-v31/styles.xml b/app/src/main/res/values-v31/styles.xml new file mode 100644 index 000000000..358806681 --- /dev/null +++ b/app/src/main/res/values-v31/styles.xml @@ -0,0 +1,65 @@ + + + + + + + - - - - + + + + diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/app/src/main/res/xml/data_extraction_rules.xml new file mode 100644 index 000000000..418b6d4c4 --- /dev/null +++ b/app/src/main/res/xml/data_extraction_rules.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/network_security_config.xml b/app/src/main/res/xml/network_security_config.xml index 84ff05a04..17fac4d1e 100644 --- a/app/src/main/res/xml/network_security_config.xml +++ b/app/src/main/res/xml/network_security_config.xml @@ -1,6 +1,5 @@ - diff --git a/app/src/main/res/xml/provider_widget_lucky_number.xml b/app/src/main/res/xml/provider_widget_lucky_number.xml index 064f20570..330bd53fe 100644 --- a/app/src/main/res/xml/provider_widget_lucky_number.xml +++ b/app/src/main/res/xml/provider_widget_lucky_number.xml @@ -4,11 +4,13 @@ android:configure="io.github.wulkanowy.ui.modules.luckynumberwidget.LuckyNumberWidgetConfigureActivity" android:initialLayout="@layout/widget_luckynumber" android:minWidth="110dp" - android:minHeight="40dp" - android:minResizeWidth="40dp" + android:minHeight="110dp" android:minResizeHeight="40dp" android:previewImage="@drawable/img_luckynumber_widget_preview" + android:previewLayout="@layout/widget_luckynumber" android:resizeMode="horizontal|vertical" + android:targetCellWidth="2" + android:targetCellHeight="2" android:updatePeriodMillis="3600000" android:widgetCategory="home_screen" - tools:targetApi="jelly_bean_mr1" /> + tools:targetApi="s" /> diff --git a/app/src/main/res/xml/provider_widget_timetable.xml b/app/src/main/res/xml/provider_widget_timetable.xml index 5392dd50b..555d8cb10 100644 --- a/app/src/main/res/xml/provider_widget_timetable.xml +++ b/app/src/main/res/xml/provider_widget_timetable.xml @@ -3,12 +3,15 @@ xmlns:tools="http://schemas.android.com/tools" android:configure="io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetConfigureActivity" android:initialLayout="@layout/widget_timetable" - android:minWidth="250dp" - android:minHeight="110dp" - android:minResizeWidth="250dp" - android:minResizeHeight="110dp" + android:minWidth="245dp" + android:minHeight="102dp" + android:minResizeWidth="245dp" + android:minResizeHeight="102dp" android:previewImage="@drawable/img_timetable_widget_preview" + android:previewLayout="@layout/widget_timetable_preview" android:resizeMode="horizontal|vertical" + android:targetCellWidth="3" + android:targetCellHeight="3" android:updatePeriodMillis="3600000" android:widgetCategory="home_screen" - tools:targetApi="jelly_bean_mr1" /> + tools:targetApi="s" /> diff --git a/app/src/main/res/xml/scheme_preferences_advanced.xml b/app/src/main/res/xml/scheme_preferences_advanced.xml index 95f6f383a..8185de81c 100644 --- a/app/src/main/res/xml/scheme_preferences_advanced.xml +++ b/app/src/main/res/xml/scheme_preferences_advanced.xml @@ -53,5 +53,12 @@ app:key="@string/pref_key_fill_message_content" app:singleLineTitle="false" app:title="@string/pref_other_fill_message_content" /> + diff --git a/app/src/main/res/xml/scheme_preferences_appearance.xml b/app/src/main/res/xml/scheme_preferences_appearance.xml index b2da0287f..9c02a4910 100644 --- a/app/src/main/res/xml/scheme_preferences_appearance.xml +++ b/app/src/main/res/xml/scheme_preferences_appearance.xml @@ -20,23 +20,21 @@ app:key="@string/pref_key_app_theme" app:title="@string/pref_view_app_theme" app:useSimpleSummaryProvider="true" /> - + app:key="@string/pref_key_menu_order" + app:summary="@string/pref_view_menu_order_summary" + app:title="@string/pref_view_menu_order_title" /> @@ -92,11 +90,6 @@ android:layout_height="wrap_content" app:iconSpaceReserved="false" app:title="@string/pref_timetable_appearance_view"> - --> + diff --git a/app/src/play/java/io/github/wulkanowy/ui/modules/settings/ads/AdsFragment.kt b/app/src/play/java/io/github/wulkanowy/ui/modules/settings/ads/AdsFragment.kt index de4c591e1..30b9e6b77 100644 --- a/app/src/play/java/io/github/wulkanowy/ui/modules/settings/ads/AdsFragment.kt +++ b/app/src/play/java/io/github/wulkanowy/ui/modules/settings/ads/AdsFragment.kt @@ -2,18 +2,17 @@ package io.github.wulkanowy.ui.modules.settings.ads import android.os.Bundle import android.view.View -import androidx.preference.CheckBoxPreference import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import androidx.preference.SwitchPreferenceCompat import com.google.android.gms.ads.rewardedinterstitial.RewardedInterstitialAd -import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R -import io.github.wulkanowy.databinding.DialogAdsConsentBinding import io.github.wulkanowy.ui.base.BaseActivity import io.github.wulkanowy.ui.base.ErrorDialog +import io.github.wulkanowy.ui.modules.auth.AuthDialog import io.github.wulkanowy.ui.modules.main.MainView +import io.github.wulkanowy.utils.AdsHelper import io.github.wulkanowy.utils.openInternetBrowser import javax.inject.Inject @@ -23,6 +22,9 @@ class AdsFragment : PreferenceFragmentCompat(), MainView.TitledView, AdsView { @Inject lateinit var presenter: AdsPresenter + @Inject + lateinit var adsHelper: AdsHelper + override val titleStringId = R.string.pref_settings_ads_title override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { @@ -45,11 +47,18 @@ class AdsFragment : PreferenceFragmentCompat(), MainView.TitledView, AdsView { true } - findPreference(getString(R.string.pref_key_ads_consent_data_processing)) - ?.setOnPreferenceChangeListener { _, newValue -> - presenter.onConsentSelected(newValue as Boolean) - true - } + findPreference(getString(R.string.pref_key_ads_ump_agreements))?.setOnPreferenceClickListener { + presenter.onUmpAgreementsSelected() + true + } + + findPreference(getString(R.string.pref_key_ads_single_support)) + ?.isEnabled = adsHelper.canShowAd + + findPreference(getString(R.string.pref_key_ads_enabled))?.setOnPreferenceChangeListener { _, newValue -> + presenter.onAdsEnabledSelected(newValue as Boolean) + true + } } override fun showAd(ad: RewardedInterstitialAd) { @@ -58,48 +67,6 @@ class AdsFragment : PreferenceFragmentCompat(), MainView.TitledView, AdsView { } } - override fun showPrivacyPolicyDialog() { - 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 @@ -134,8 +101,16 @@ class AdsFragment : PreferenceFragmentCompat(), MainView.TitledView, AdsView { (activity as? BaseActivity<*, *>)?.showMessage(text) } - override fun showExpiredDialog() { - (activity as? BaseActivity<*, *>)?.showExpiredDialog() + override fun showExpiredCredentialsDialog() { + (activity as? BaseActivity<*, *>)?.showExpiredCredentialsDialog() + } + + override fun onCaptchaVerificationRequired(url: String?) { + (activity as? BaseActivity<*, *>)?.onCaptchaVerificationRequired(url) + } + + override fun showDecryptionFailedDialog() { + (activity as? BaseActivity<*, *>)?.showDecryptionFailedDialog() } override fun showChangePasswordSnackbar(redirectUrl: String) { @@ -146,6 +121,10 @@ class AdsFragment : PreferenceFragmentCompat(), MainView.TitledView, AdsView { (activity as? BaseActivity<*, *>)?.openClearLoginView() } + override fun showAuthDialog() { + AuthDialog.newInstance().show(childFragmentManager, "auth_dialog") + } + override fun showErrorDetailsDialog(error: Throwable) { ErrorDialog.newInstance(error).show(childFragmentManager, error.toString()) } 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 772d616d7..b3c701541 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,6 +1,5 @@ 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 @@ -13,40 +12,34 @@ class AdsPresenter @Inject constructor( errorHandler: ErrorHandler, studentRepository: StudentRepository, 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?.showLoadingSupportAd(true) presenterScope.launch { runCatching { adsHelper.getSupportAd() } - .onFailure(errorHandler::dispatch) - .onSuccess { it?.let { view?.showAd(it) } } + .onFailure { + errorHandler.dispatch(it) - view?.run { - showLoadingSupportAd(false) - showWatchAdOncePerVisit(true) - } - } - } + view?.run { + showLoadingSupportAd(false) + showWatchAdOncePerVisit(false) + } + } + .onSuccess { + it?.let { view?.showAd(it) } - fun onConsentSelected(isChecked: Boolean) { - if (isChecked) { - view?.showPrivacyPolicyDialog() - } else { - view?.showProcessingDataSummary(null) - view?.setCheckedAdsEnabled(false) + view?.run { + showLoadingSupportAd(false) + showWatchAdOncePerVisit(true) + } + } } } @@ -54,25 +47,13 @@ class AdsPresenter @Inject constructor( view?.openPrivacyPolicy() } - fun onPrivacyDialogCanceled() { - view?.setCheckedProcessingData(false) + fun onAdsEnabledSelected(newValue: Boolean) { + if (newValue) { + adsHelper.initialize() + } } - 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) + fun onUmpAgreementsSelected() { + adsHelper.openAdsUmpAgreements() } } 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 8de6e60d3..3b3fa5783 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 @@ -9,8 +9,6 @@ interface AdsView : BaseView { fun showAd(ad: RewardedInterstitialAd) - fun showPrivacyPolicyDialog() - fun openPrivacyPolicy() fun showLoadingSupportAd(show: Boolean) @@ -18,8 +16,4 @@ interface AdsView : BaseView { fun showWatchAdOncePerVisit(show: Boolean) 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 c536e2218..bd17d52c1 100644 --- a/app/src/play/java/io/github/wulkanowy/utils/AdsHelper.kt +++ b/app/src/play/java/io/github/wulkanowy/utils/AdsHelper.kt @@ -1,40 +1,110 @@ package io.github.wulkanowy.utils +import android.app.Activity import android.content.Context -import android.os.Bundle +import android.net.ConnectivityManager +import android.net.NetworkCapabilities +import android.os.Build import android.view.View -import com.google.ads.mediation.admob.AdMobAdapter -import com.google.android.gms.ads.* +import androidx.core.content.getSystemService +import com.google.android.gms.ads.AdListener +import com.google.android.gms.ads.AdRequest +import com.google.android.gms.ads.AdSize +import com.google.android.gms.ads.AdView +import com.google.android.gms.ads.LoadAdError +import com.google.android.gms.ads.MobileAds import com.google.android.gms.ads.rewardedinterstitial.RewardedInterstitialAd import com.google.android.gms.ads.rewardedinterstitial.RewardedInterstitialAdLoadCallback +import com.google.android.ump.ConsentInformation +import com.google.android.ump.ConsentRequestParameters +import com.google.android.ump.UserMessagingPlatform import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.android.scopes.ActivityScoped import io.github.wulkanowy.BuildConfig import io.github.wulkanowy.data.repositories.PreferencesRepository +import kotlinx.coroutines.flow.MutableStateFlow +import timber.log.Timber +import java.net.UnknownHostException +import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException import kotlin.coroutines.suspendCoroutine - +@ActivityScoped class AdsHelper @Inject constructor( + private val activity: Activity, @ApplicationContext private val context: Context, - private val preferencesRepository: PreferencesRepository + preferencesRepository: PreferencesRepository ) { + private var isMobileAdsInitializeCalled = AtomicBoolean(false) + private var consentInformation: ConsentInformation? = null + + private val canRequestAd get() = consentInformation?.canRequestAds() == true + val isMobileAdsSdkInitialized = MutableStateFlow(false) + val canShowAd get() = isMobileAdsSdkInitialized.value && canRequestAd + + init { + if (preferencesRepository.isAdsEnabled) { + initialize() + } + } + fun initialize() { - if (preferencesRepository.isAgreeToProcessData) { - MobileAds.initialize(context) + val consentRequestParameters = ConsentRequestParameters.Builder() + .build() + + consentInformation = UserMessagingPlatform.getConsentInformation(context) + consentInformation?.requestConsentInfoUpdate( + activity, + consentRequestParameters, + { + UserMessagingPlatform.loadAndShowConsentFormIfRequired( + activity + ) { loadAndShowError -> + + if (loadAndShowError != null) { + Timber.e(IllegalStateException("${loadAndShowError.errorCode}: ${loadAndShowError.message}")) + } + + if (canRequestAd) { + initializeMobileAds() + } + } + }, + { requestConsentError -> + Timber.e(IllegalStateException("${requestConsentError.errorCode}: ${requestConsentError.message}")) + }) + + if (canRequestAd) { + initializeMobileAds() + } + } + + fun openAdsUmpAgreements() { + UserMessagingPlatform.showPrivacyOptionsForm(activity) { + if (it != null) { + Timber.e(IllegalStateException("${it.errorCode}: ${it.message}")) + } + } + } + + private fun initializeMobileAds() { + if (isMobileAdsInitializeCalled.getAndSet(true)) return + + MobileAds.initialize(context) { + isMobileAdsSdkInitialized.value = true } } suspend fun getSupportAd(): RewardedInterstitialAd? { - val extra = Bundle().apply { putString("npa", "1") } + if (!canRequestAd) return null + if (!context.isInternetConnected()) { + throw UnknownHostException() + } + val adRequest = AdRequest.Builder() - .apply { - if (!preferencesRepository.isPersonalizedAdsEnabled) { - addNetworkExtrasBundle(AdMobAdapter::class.java, extra) - } - } .build() return suspendCoroutine { @@ -55,13 +125,8 @@ class AdsHelper @Inject constructor( } suspend fun getDashboardTileAdBanner(width: Int): AdBanner { - val extra = Bundle().apply { putString("npa", "1") } + if (!canShowAd) throw IllegalStateException("Cannot show ad") val adRequest = AdRequest.Builder() - .apply { - if (!preferencesRepository.isPersonalizedAdsEnabled) { - addNetworkExtrasBundle(AdMobAdapter::class.java, extra) - } - } .build() return suspendCoroutine { @@ -84,4 +149,18 @@ class AdsHelper @Inject constructor( } } +@Suppress("DEPRECATION") +private fun Context.isInternetConnected(): Boolean { + val connectivityManager = getSystemService() ?: return false + + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + val currentNetwork = connectivityManager.activeNetwork + val networkCapabilities = connectivityManager.getNetworkCapabilities(currentNetwork) + + networkCapabilities?.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) == true + } else { + connectivityManager.activeNetworkInfo?.isConnected == true + } +} + data class AdBanner(val view: View) diff --git a/app/src/play/java/io/github/wulkanowy/utils/AnalyticsHelper.kt b/app/src/play/java/io/github/wulkanowy/utils/AnalyticsHelper.kt index b65325790..9ded7e1b6 100644 --- a/app/src/play/java/io/github/wulkanowy/utils/AnalyticsHelper.kt +++ b/app/src/play/java/io/github/wulkanowy/utils/AnalyticsHelper.kt @@ -1,28 +1,39 @@ package io.github.wulkanowy.utils import android.app.Activity -import android.content.Context import android.os.Bundle +import com.google.firebase.Firebase import com.google.firebase.analytics.FirebaseAnalytics -import dagger.hilt.android.qualifiers.ApplicationContext +import com.google.firebase.analytics.analytics +import com.google.firebase.crashlytics.crashlytics +import io.github.wulkanowy.data.repositories.PreferencesRepository import javax.inject.Inject import javax.inject.Singleton @Singleton class AnalyticsHelper @Inject constructor( - @ApplicationContext private val context: Context + preferencesRepository: PreferencesRepository, + appInfo: AppInfo, ) { - private val analytics by lazy { FirebaseAnalytics.getInstance(context) } + private val analytics by lazy { Firebase.analytics } + + private val crashlytics by lazy { Firebase.crashlytics } + + init { + if (!appInfo.isDebug) { + crashlytics.setUserId(preferencesRepository.installationId) + } + } fun logEvent(name: String, vararg params: Pair) { Bundle().apply { - params.forEach { - if (it.second == null) return@forEach - when (it.second) { - is String, is String? -> putString(it.first, it.second.toString()) - is Int, is Int? -> putInt(it.first, it.second as Int) - is Boolean, is Boolean? -> putBoolean(it.first, it.second as Boolean) + params.forEach { (key, value) -> + if (value == null) return@forEach + when (value) { + is String -> putString(key, value) + is Int -> putInt(key, value) + is Boolean -> putBoolean(key, value) } } analytics.logEvent(name, this) diff --git a/app/src/play/java/io/github/wulkanowy/utils/CrashlyticsUtils.kt b/app/src/play/java/io/github/wulkanowy/utils/CrashlyticsUtils.kt index 410fddf16..2e5fad622 100644 --- a/app/src/play/java/io/github/wulkanowy/utils/CrashlyticsUtils.kt +++ b/app/src/play/java/io/github/wulkanowy/utils/CrashlyticsUtils.kt @@ -2,8 +2,8 @@ package io.github.wulkanowy.utils import android.util.Log import com.google.firebase.crashlytics.FirebaseCrashlytics -import fr.bipi.tressence.base.FormatterPriorityTree -import fr.bipi.tressence.common.StackTraceRecorder +import fr.bipi.treessence.base.FormatterPriorityTree +import fr.bipi.treessence.common.StackTraceRecorder class CrashLogTree : FormatterPriorityTree(Log.VERBOSE) { @@ -23,9 +23,6 @@ class CrashLogExceptionTree : FormatterPriorityTree(Log.ERROR, ExceptionFilter) override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { if (skipLog(priority, tag, message, t)) return - crashlytics.setCustomKey("priority", priority) - crashlytics.setCustomKey("tag", tag.orEmpty()) - crashlytics.setCustomKey("message", message) if (t != null) { crashlytics.recordException(t) } else { diff --git a/app/src/play/java/io/github/wulkanowy/utils/UpdateHelper.kt b/app/src/play/java/io/github/wulkanowy/utils/InAppUpdateHelper.kt similarity index 76% rename from app/src/play/java/io/github/wulkanowy/utils/UpdateHelper.kt rename to app/src/play/java/io/github/wulkanowy/utils/InAppUpdateHelper.kt index 6772237e2..d89dfd5f8 100644 --- a/app/src/play/java/io/github/wulkanowy/utils/UpdateHelper.kt +++ b/app/src/play/java/io/github/wulkanowy/utils/InAppUpdateHelper.kt @@ -3,12 +3,15 @@ package io.github.wulkanowy.utils import android.app.Activity import android.app.Activity.RESULT_OK import android.content.Context -import android.content.IntentSender import android.view.View import android.widget.Toast +import androidx.activity.result.ActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AppCompatActivity import com.google.android.material.snackbar.Snackbar import com.google.android.play.core.appupdate.AppUpdateInfo import com.google.android.play.core.appupdate.AppUpdateManagerFactory +import com.google.android.play.core.appupdate.AppUpdateOptions import com.google.android.play.core.install.InstallStateUpdatedListener import com.google.android.play.core.install.model.AppUpdateType.FLEXIBLE import com.google.android.play.core.install.model.AppUpdateType.IMMEDIATE @@ -20,15 +23,16 @@ import com.google.android.play.core.ktx.isFlexibleUpdateAllowed import com.google.android.play.core.ktx.isImmediateUpdateAllowed import com.google.android.play.core.ktx.updatePriority import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.android.scopes.ActivityScoped import io.github.wulkanowy.R import timber.log.Timber import javax.inject.Inject -import javax.inject.Singleton -@Singleton -class UpdateHelper @Inject constructor( +@ActivityScoped +class InAppUpdateHelper @Inject constructor( @ApplicationContext private val context: Context, private val analyticsHelper: AnalyticsHelper, + activity: Activity ) { lateinit var messageContainer: View @@ -39,6 +43,7 @@ class UpdateHelper @Inject constructor( when (state.installStatus()) { PENDING -> Toast.makeText(context, R.string.update_download_started, Toast.LENGTH_SHORT) .show() + DOWNLOADED -> popupSnackBarForCompleteUpdate() else -> Timber.d("Update state: ${state.installStatus()}") } @@ -70,45 +75,55 @@ class UpdateHelper @Inject constructor( return updateAvailability() == UPDATE_AVAILABLE && isFlexibleUpdateAllowed && isUpdatePriorityAllowUpdate } - fun checkAndInstallUpdates(activity: Activity) { + private val activityResultLauncher = (activity as AppCompatActivity).registerForActivityResult( + ActivityResultContracts.StartIntentSenderForResult(), + ::onActivityResult + ) + + fun checkAndInstallUpdates() { Timber.d("Checking for updates...") appUpdateManager.appUpdateInfo.addOnSuccessListener { appUpdateInfo -> when { appUpdateInfo.isImmediateUpdateAvailable -> { - startUpdate(activity, appUpdateInfo, IMMEDIATE) + startUpdate(appUpdateInfo, IMMEDIATE) } + appUpdateInfo.isFlexibleUpdateAvailable -> { appUpdateManager.registerListener(flexibleUpdateListener) - startUpdate(activity, appUpdateInfo, FLEXIBLE) + startUpdate(appUpdateInfo, FLEXIBLE) } + else -> Timber.d("No update available") } } } - private fun startUpdate(activity: Activity, appUpdateInfo: AppUpdateInfo, updateType: Int) { + private fun startUpdate(appUpdateInfo: AppUpdateInfo, updateType: Int) { Timber.d("Start update ($updateType): $appUpdateInfo") + try { appUpdateManager.startUpdateFlowForResult( - appUpdateInfo, updateType, activity, IN_APP_UPDATE_REQUEST_CODE + appUpdateInfo, + activityResultLauncher, + AppUpdateOptions.defaultOptions(updateType) ) - } catch (e: IntentSender.SendIntentException) { - Timber.i("Update failed! Duplicated PendingIntent") + } catch (e: Exception) { + Timber.e(e, "Update failed!") } } - fun onActivityResult(requestCode: Int, resultCode: Int) { - if (requestCode == IN_APP_UPDATE_REQUEST_CODE) { - if (resultCode != RESULT_OK) { - Timber.i("Update failed! Result code: $resultCode") - Toast.makeText(context, R.string.update_failed, Toast.LENGTH_LONG).show() - } + private fun onActivityResult(activityResult: ActivityResult) { + val resultCode = activityResult.resultCode - analyticsHelper.logEvent("inapp_update", "code" to resultCode) + if (resultCode != RESULT_OK) { + Timber.i("Update failed! Result code: $resultCode") + Toast.makeText(context, R.string.update_failed, Toast.LENGTH_LONG).show() } + + analyticsHelper.logEvent("inapp_update", "code" to resultCode) } - fun onResume(activity: Activity) { + fun onResume() { appUpdateManager.appUpdateInfo.addOnSuccessListener { info -> Timber.d("InAppUpdate.onResume() listener: $info") @@ -116,7 +131,6 @@ class UpdateHelper @Inject constructor( DOWNLOADED == info.installStatus() -> popupSnackBarForCompleteUpdate() DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS == info.updateAvailability() -> { startUpdate( - activity = activity, appUpdateInfo = info, updateType = if (info.isImmediateUpdateAvailable) IMMEDIATE else FLEXIBLE ) @@ -139,9 +153,4 @@ class UpdateHelper @Inject constructor( show() } } - - private companion object { - - private const val IN_APP_UPDATE_REQUEST_CODE = 1721 - } } diff --git a/app/src/play/java/io/github/wulkanowy/utils/IntegrityHelper.kt b/app/src/play/java/io/github/wulkanowy/utils/IntegrityHelper.kt new file mode 100644 index 000000000..41df0487a --- /dev/null +++ b/app/src/play/java/io/github/wulkanowy/utils/IntegrityHelper.kt @@ -0,0 +1,27 @@ +package io.github.wulkanowy.utils + +import android.content.Context +import com.google.android.play.core.integrity.IntegrityManagerFactory +import com.google.android.play.core.integrity.IntegrityTokenRequest +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.tasks.await +import java.util.UUID +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class IntegrityHelper @Inject constructor( + @ApplicationContext private val context: Context, +) { + + suspend fun getIntegrityToken(nonce: String): String? { + val integrityManager = IntegrityManagerFactory.create(context) + + val integrityTokenResponse = integrityManager.requestIntegrityToken( + IntegrityTokenRequest.builder() + .setNonce(nonce) + .build() + ) + return integrityTokenResponse.await().token() + } +} diff --git a/app/src/play/java/io/github/wulkanowy/utils/RemoteConfigHelper.kt b/app/src/play/java/io/github/wulkanowy/utils/RemoteConfigHelper.kt new file mode 100644 index 000000000..d17bfb4eb --- /dev/null +++ b/app/src/play/java/io/github/wulkanowy/utils/RemoteConfigHelper.kt @@ -0,0 +1,35 @@ +package io.github.wulkanowy.utils + +import android.content.Context +import com.google.firebase.FirebaseApp +import com.google.firebase.ktx.Firebase +import com.google.firebase.remoteconfig.ktx.remoteConfig +import com.google.firebase.remoteconfig.ktx.remoteConfigSettings +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class RemoteConfigHelper @Inject constructor( + @ApplicationContext private val context: Context, + private val appInfo: AppInfo, +) : BaseRemoteConfigHelper() { + + override fun initialize() { + FirebaseApp.initializeApp(context) + + Firebase.remoteConfig.setConfigSettingsAsync(remoteConfigSettings { + fetchTimeoutInSeconds = 3 + if (appInfo.isDebug) { + minimumFetchIntervalInSeconds = 0 + } + }) + Firebase.remoteConfig.setDefaultsAsync(RemoteConfigDefaults.values().associate { + it.key to it.value + }) + Firebase.remoteConfig.fetchAndActivate() + } + + override val userAgentTemplate: String + get() = Firebase.remoteConfig.getString(RemoteConfigDefaults.USER_AGENT_TEMPLATE.key) +} diff --git a/app/src/play/res/xml/scheme_preferences_ads.xml b/app/src/play/res/xml/scheme_preferences_ads.xml index 4165561a7..444dde3e4 100644 --- a/app/src/play/res/xml/scheme_preferences_ads.xml +++ b/app/src/play/res/xml/scheme_preferences_ads.xml @@ -1,5 +1,11 @@ + @@ -8,25 +14,17 @@ app:key="@string/pref_key_ads_privacy_policy" app:singleLineTitle="false" app:title="@string/pref_ads_privacy_policy" /> - - > { @@ -193,7 +198,13 @@ class Migration13Test : AbstractMigrationTest() { }) } - private fun createSemester(db: SupportSQLiteDatabase, studentId: Int, classId: Int, semesterId: Int, diaryId: Int, isCurrent: Boolean = false) { + private fun createSemester( + db: SupportSQLiteDatabase, + studentId: Int, + semesterId: Int, + diaryId: Int, + isCurrent: Boolean = false + ) { db.insert("Semesters", SQLiteDatabase.CONFLICT_FAIL, ContentValues().apply { put("student_id", studentId) put("diary_id", diaryId) @@ -201,7 +212,7 @@ class Migration13Test : AbstractMigrationTest() { put("semester_id", semesterId) put("semester_name", "1") put("is_current", isCurrent) - put("class_id", classId) + put("class_id", 5) put("unit_id", "99") }) } diff --git a/app/src/test/java/io/github/wulkanowy/data/db/migrations/Migration27Test.kt b/app/src/test/java/io/github/wulkanowy/data/db/migrations/Migration27Test.kt index 8e744f27a..19eda9ba8 100644 --- a/app/src/test/java/io/github/wulkanowy/data/db/migrations/Migration27Test.kt +++ b/app/src/test/java/io/github/wulkanowy/data/db/migrations/Migration27Test.kt @@ -27,7 +27,7 @@ class Migration27Test : AbstractMigrationTest() { close() } - helper.runMigrationsAndValidate(dbName, 27, true, Migration27()) + runMigrationsAndValidate(Migration27()) val db = getMigratedRoomDatabase() val students = runBlocking { db.studentDao.loadAll() } @@ -39,6 +39,8 @@ class Migration27Test : AbstractMigrationTest() { assertEquals(123, userLoginId) assertEquals("Student Jan", userName) } + + db.close() } @Test @@ -49,7 +51,7 @@ class Migration27Test : AbstractMigrationTest() { close() } - helper.runMigrationsAndValidate(dbName, 27, true, Migration27()) + runMigrationsAndValidate(Migration27()) val db = getMigratedRoomDatabase() val students = runBlocking { db.studentDao.loadAll() } @@ -61,6 +63,8 @@ class Migration27Test : AbstractMigrationTest() { assertEquals(2, userLoginId) assertEquals("Unit Jan", userName) } + + db.close() } @Test @@ -73,7 +77,7 @@ class Migration27Test : AbstractMigrationTest() { close() } - helper.runMigrationsAndValidate(dbName, 27, true, Migration27()) + runMigrationsAndValidate(Migration27()) val db = getMigratedRoomDatabase() val students = runBlocking { db.studentDao.loadAll() } @@ -90,6 +94,8 @@ class Migration27Test : AbstractMigrationTest() { assertEquals(333, userLoginId) assertEquals("Unit Tomasz", userName) } + + db.close() } private fun createStudent(db: SupportSQLiteDatabase, id: Long, userLoginId: Int, studentName: String) { diff --git a/app/src/test/java/io/github/wulkanowy/data/db/migrations/Migration35Test.kt b/app/src/test/java/io/github/wulkanowy/data/db/migrations/Migration35Test.kt index 883cdb81c..79c24f2e6 100644 --- a/app/src/test/java/io/github/wulkanowy/data/db/migrations/Migration35Test.kt +++ b/app/src/test/java/io/github/wulkanowy/data/db/migrations/Migration35Test.kt @@ -29,7 +29,7 @@ class Migration35Test : AbstractMigrationTest() { close() } - helper.runMigrationsAndValidate(dbName, 35, true, Migration35(AppInfo())) + runMigrationsAndValidate(Migration35(AppInfo())) val db = getMigratedRoomDatabase() val students = runBlocking { db.studentDao.loadAll() } @@ -38,6 +38,8 @@ class Migration35Test : AbstractMigrationTest() { assertTrue { students[0].avatarColor in AppInfo().defaultColorsForAvatar } assertTrue { students[1].avatarColor in AppInfo().defaultColorsForAvatar } + + db.close() } private fun createStudent(db: SupportSQLiteDatabase, id: Long) { diff --git a/app/src/test/java/io/github/wulkanowy/data/db/migrations/Migration54Test.kt b/app/src/test/java/io/github/wulkanowy/data/db/migrations/Migration54Test.kt new file mode 100644 index 000000000..1855e0d50 --- /dev/null +++ b/app/src/test/java/io/github/wulkanowy/data/db/migrations/Migration54Test.kt @@ -0,0 +1,130 @@ +package io.github.wulkanowy.data.db.migrations + +import android.content.ContentValues +import android.database.sqlite.SQLiteDatabase +import android.os.Build +import androidx.sqlite.db.SupportSQLiteDatabase +import dagger.hilt.android.testing.HiltAndroidTest +import dagger.hilt.android.testing.HiltTestApplication +import io.github.wulkanowy.sdk.Sdk +import io.github.wulkanowy.sdk.Sdk.ScrapperLoginType.* +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import kotlin.random.Random +import kotlin.test.assertEquals + +@HiltAndroidTest +@RunWith(RobolectricTestRunner::class) +@OptIn(ExperimentalCoroutinesApi::class) +@Config(sdk = [Build.VERSION_CODES.O_MR1], application = HiltTestApplication::class) +class Migration54Test : AbstractMigrationTest() { + + @Test + fun `don't touch unrelated students`() = runTest { + with(helper.createDatabase(dbName, 53)) { + createStudent(1, STANDARD, "vulcan.net.pl", "rzeszow", "Jan Michniewicz") + createStudent(2, ADFSLight, "umt.tarnow.pl", "tarnow", "Joanna Marcinkiewicz") + close() + } + + runMigrationsAndValidate(Migration54()) + val db = getMigratedRoomDatabase() + val students = db.studentDao.loadAll() + + assertEquals(2, students.size) + with(students[0]) { + assertEquals(STANDARD.name, loginType) + assertEquals("https://vulcan.net.pl", scrapperBaseUrl) + assertEquals("rzeszow", symbol) + } + with(students[1]) { + assertEquals(ADFSLight.name, loginType) + assertEquals("https://umt.tarnow.pl", scrapperBaseUrl) + assertEquals("tarnow", symbol) + } + db.close() + } + + @Test + fun `remove tomaszow mazowiecki students`() = runTest { + with(helper.createDatabase(dbName, 53)) { + createStudent(1, STANDARD, "vulcan.net.pl", "rzeszow", "Jan Michniewicz") + createStudent(2, STANDARD, "vulcan.net.pl", "tomaszowmazowiecki", "Joanna Stec") + createStudent(3, STANDARD, "vulcan.net.pl", "tomaszowmazowiecki", "Kacper Morawiecki") + close() + } + + runMigrationsAndValidate(Migration54()) + val db = getMigratedRoomDatabase() + val students = db.studentDao.loadAll() + assertEquals(1, students.size) + with(students[0]) { + assertEquals("rzeszow", symbol) + } + db.close() + } + + @Test + fun `migrate resman students`() = runTest { + with(helper.createDatabase(dbName, 53)) { + createStudent(1, ADFSLight, "resman.pl", "rzeszow", "Joanna Stec") + createStudent(2, ADFSLight, "resman.pl", "rzeszow", "Kacper Morawiecki") + createStudent(3, STANDARD, "vulcan.net.pl", "rzeszow", "Jan Michniewicz") + close() + } + runMigrationsAndValidate(Migration54()) + val db = getMigratedRoomDatabase() + val students = db.studentDao.loadAll() + assertEquals(3, students.size) + with(students[0]) { + assertEquals(ADFSLightScoped.name, loginType) + assertEquals("https://vulcan.net.pl", scrapperBaseUrl) + assertEquals("rzeszowprojekt", symbol) + } + with(students[1]) { + assertEquals(ADFSLightScoped.name, loginType) + assertEquals("https://vulcan.net.pl", scrapperBaseUrl) + assertEquals("rzeszowprojekt", symbol) + } + db.close() + } + + private fun SupportSQLiteDatabase.createStudent( + id: Long, + loginType: Sdk.ScrapperLoginType, + host: String, + symbol: String, + studentName: String, + ) { + insert("Students", SQLiteDatabase.CONFLICT_FAIL, ContentValues().apply { + put("scrapper_base_url", "https://$host") + put("mobile_base_url", "") + put("login_type", loginType.name) + put("login_mode", "SCRAPPER") + put("certificate_key", "") + put("private_key", "") + put("is_parent", false) + put("email", "jan@fakelog.cf") + put("password", "******") + put("symbol", symbol) + put("student_id", Random.nextInt()) + put("user_login_id", id) + put("user_name", studentName) + put("student_name", studentName) + put("school_id", "123") + put("school_short", "") + put("school_name", "") + put("class_name", "") + put("class_id", Random.nextInt()) + put("is_current", false) + put("registration_date", "0") + put("id", id) + put("nick", "") + put("avatar_color", "") + }) + } +} diff --git a/app/src/test/java/io/github/wulkanowy/data/mappers/AttendanceMapperKtTest.kt b/app/src/test/java/io/github/wulkanowy/data/mappers/AttendanceMapperKtTest.kt new file mode 100644 index 000000000..ac73becdc --- /dev/null +++ b/app/src/test/java/io/github/wulkanowy/data/mappers/AttendanceMapperKtTest.kt @@ -0,0 +1,143 @@ +package io.github.wulkanowy.data.mappers + +import io.github.wulkanowy.data.db.entities.Semester +import io.github.wulkanowy.data.db.entities.Timetable +import io.github.wulkanowy.sdk.pojo.Attendance +import io.github.wulkanowy.sdk.scrapper.attendance.SentExcuseStatus +import org.junit.Test +import java.time.Instant +import java.time.LocalDate +import kotlin.test.assertEquals + +class AttendanceMapperTest { + + @Test + fun `map attendance when fallback is not necessary`() { + val attendance = listOf( + getSdkAttendance(1, LocalDate.of(2022, 11, 17), "Oryginalna 1"), + getSdkAttendance(2, LocalDate.of(2022, 11, 17), "Oryginalna 2"), + ) + val lessons = listOf( + getEntityTimetable(1, LocalDate.of(2022, 11, 17), "Pierwsza"), + getEntityTimetable(2, LocalDate.of(2022, 11, 17), "Druga"), + ) + + val result = attendance.mapToEntities(getEntitySemester(), lessons) + assertEquals("Oryginalna 1", result[0].subject) + assertEquals("Oryginalna 2", result[1].subject) + } + + @Test + fun `map attendance when fallback is not always necessary`() { + val attendance = listOf( + getSdkAttendance(1, LocalDate.of(2022, 11, 17), "Oryginalna 1"), + getSdkAttendance(2, LocalDate.of(2022, 11, 17), ""), + ) + val lessons = listOf( + getEntityTimetable(1, LocalDate.of(2022, 11, 17), "Pierwsza"), + getEntityTimetable(2, LocalDate.of(2022, 11, 17), "Druga"), + ) + + val result = attendance.mapToEntities(getEntitySemester(), lessons) + assertEquals("Oryginalna 1", result[0].subject) + assertEquals("Druga", result[1].subject) + } + + @Test + fun `map attendance when fallback is sometimes empty`() { + val attendance = listOf( + getSdkAttendance(1, LocalDate.of(2022, 11, 17), "Oryginalna 1"), + getSdkAttendance(2, LocalDate.of(2022, 11, 17), ""), + ) + val lessons = listOf( + getEntityTimetable(1, LocalDate.of(2022, 11, 17), "Pierwsza"), + ) + + val result = attendance.mapToEntities(getEntitySemester(), lessons) + assertEquals("Oryginalna 1", result[0].subject) + assertEquals("", result[1].subject) + } + + @Test + fun `map attendance when fallback is empty`() { + val attendance = listOf( + getSdkAttendance(1, LocalDate.of(2022, 11, 17), ""), + getSdkAttendance(2, LocalDate.of(2022, 11, 17), ""), + ) + val lessons = listOf( + getEntityTimetable(1, LocalDate.of(2022, 11, 18), "Pierwsza"), + getEntityTimetable(2, LocalDate.of(2022, 10, 17), "Druga"), + ) + + val result = attendance.mapToEntities(getEntitySemester(), lessons) + assertEquals("", result[0].subject) + assertEquals("", result[1].subject) + } + + @Test + fun `map attendance with all subject fallback`() { + val attendance = listOf( + getSdkAttendance(1, LocalDate.of(2022, 11, 17)), + getSdkAttendance(2, LocalDate.of(2022, 11, 17)), + ) + val lessons = listOf( + getEntityTimetable(1, LocalDate.of(2022, 11, 17), "Pierwsza"), + getEntityTimetable(2, LocalDate.of(2022, 11, 17), "Druga"), + ) + + val result = attendance.mapToEntities(getEntitySemester(), lessons) + assertEquals("Pierwsza", result[0].subject) + assertEquals("Druga", result[1].subject) + } + + private fun getSdkAttendance(number: Int, date: LocalDate, subject: String = "") = Attendance( + number = number, + name = "ABSENCE", + subject = subject, + date = date, + timeId = 1, + categoryId = 1, + deleted = false, + excuseStatus = SentExcuseStatus.WAITING, + excusable = false, + absence = false, + excused = false, + exemption = false, + lateness = false, + presence = false, + ) + + private fun getEntityTimetable(number: Int, date: LocalDate, subject: String = "") = Timetable( + number = number, + start = Instant.now(), + end = Instant.now(), + date = date, + subject = subject, + subjectOld = "", + group = "", + room = "", + roomOld = "", + teacher = "", + teacherOld = "", + info = "", + changes = false, + canceled = false, + studentId = 0, + diaryId = 0, + isStudentPlan = false, + ) + + private fun getEntitySemester() = Semester( + studentId = 0, + diaryId = 0, + kindergartenDiaryId = 0, + diaryName = "", + schoolYear = 0, + semesterId = 0, + semesterName = 0, + start = LocalDate.now(), + end = LocalDate.now(), + classId = 0, + unitId = 0 + ) +} 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 7d22f7265..d0e500f19 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 @@ -2,6 +2,7 @@ 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.db.dao.TimetableDao import io.github.wulkanowy.data.errorOrNull import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.data.toFirstResult @@ -29,6 +30,9 @@ class AttendanceRepositoryTest { @MockK private lateinit var attendanceDb: AttendanceDao + @MockK + private lateinit var timetableDb: TimetableDao + @MockK(relaxUnitFun = true) private lateinit var refreshHelper: AutoRefreshHelper @@ -51,17 +55,18 @@ class AttendanceRepositoryTest { fun setUp() { MockKAnnotations.init(this) every { refreshHelper.shouldBeRefreshed(any()) } returns false + coEvery { timetableDb.load(any(), any(), any(), any()) } returns emptyList() - attendanceRepository = AttendanceRepository(attendanceDb, sdk, refreshHelper) + attendanceRepository = AttendanceRepository(attendanceDb, timetableDb, sdk, refreshHelper) } @Test fun `force refresh without difference`() { // prepare - coEvery { sdk.getAttendance(startDate, endDate, 1) } returns remoteList + coEvery { sdk.getAttendance(startDate, endDate) } returns remoteList coEvery { attendanceDb.loadAll(1, 1, startDate, endDate) } returnsMany listOf( - flowOf(remoteList.mapToEntities(semester)), - flowOf(remoteList.mapToEntities(semester)) + flowOf(remoteList.mapToEntities(semester, emptyList())), + flowOf(remoteList.mapToEntities(semester, emptyList())) ) coEvery { attendanceDb.insertAll(any()) } returns listOf(1, 2, 3) coEvery { attendanceDb.deleteAll(any()) } just Runs @@ -72,7 +77,7 @@ class AttendanceRepositoryTest { // verify assertEquals(null, res.errorOrNull) assertEquals(2, res.dataOrNull?.size) - coVerify { sdk.getAttendance(startDate, endDate, 1) } + coVerify { sdk.getAttendance(startDate, endDate) } coVerify { attendanceDb.loadAll(1, 1, startDate, endDate) } coVerify { attendanceDb.insertAll(match { it.isEmpty() }) } coVerify { attendanceDb.deleteAll(match { it.isEmpty() }) } @@ -81,11 +86,11 @@ class AttendanceRepositoryTest { @Test fun `force refresh with more items in remote`() { // prepare - coEvery { sdk.getAttendance(startDate, endDate, 1) } returns remoteList + coEvery { sdk.getAttendance(startDate, endDate) } returns remoteList coEvery { attendanceDb.loadAll(1, 1, startDate, endDate) } 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(semester, emptyList())), + flowOf(remoteList.dropLast(1).mapToEntities(semester, emptyList())), // after fetch end before save result + flowOf(remoteList.mapToEntities(semester, emptyList())) ) coEvery { attendanceDb.insertAll(any()) } returns listOf(1, 2, 3) coEvery { attendanceDb.deleteAll(any()) } just Runs @@ -96,11 +101,11 @@ class AttendanceRepositoryTest { // verify assertEquals(null, res.errorOrNull) assertEquals(2, res.dataOrNull?.size) - coVerify { sdk.getAttendance(startDate, endDate, 1) } + coVerify { sdk.getAttendance(startDate, endDate) } coVerify { attendanceDb.loadAll(1, 1, startDate, endDate) } coVerify { attendanceDb.insertAll(match { - it.size == 1 && it[0] == remoteList.mapToEntities(semester)[1] + it.size == 1 && it[0] == remoteList.mapToEntities(semester, emptyList())[1] }) } coVerify { attendanceDb.deleteAll(match { it.isEmpty() }) } @@ -109,11 +114,11 @@ class AttendanceRepositoryTest { @Test fun `force refresh with more items in local`() { // prepare - coEvery { sdk.getAttendance(startDate, endDate, 1) } returns remoteList.dropLast(1) + coEvery { sdk.getAttendance(startDate, endDate) } returns remoteList.dropLast(1) coEvery { attendanceDb.loadAll(1, 1, startDate, endDate) } 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(semester, emptyList())), + flowOf(remoteList.mapToEntities(semester, emptyList())), // after fetch end before save result + flowOf(remoteList.dropLast(1).mapToEntities(semester, emptyList())) ) coEvery { attendanceDb.insertAll(any()) } returns listOf(1, 2, 3) coEvery { attendanceDb.deleteAll(any()) } just Runs @@ -124,12 +129,12 @@ class AttendanceRepositoryTest { // verify assertEquals(null, res.errorOrNull) assertEquals(1, res.dataOrNull?.size) - coVerify { sdk.getAttendance(startDate, endDate, 1) } + coVerify { sdk.getAttendance(startDate, endDate) } coVerify { attendanceDb.loadAll(1, 1, startDate, endDate) } coVerify { attendanceDb.insertAll(match { it.isEmpty() }) } coVerify { attendanceDb.deleteAll(match { - it.size == 1 && it[0] == remoteList.mapToEntities(semester)[1] + it.size == 1 && it[0] == remoteList.mapToEntities(semester, emptyList())[1] }) } } 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 e3790662e..fb037a87e 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 @@ -59,7 +59,7 @@ class ExamRemoteTest { @Test fun `force refresh without difference`() { // prepare - coEvery { sdk.getExams(startDate, realEndDate, 1) } returns remoteList + coEvery { sdk.getExams(startDate, realEndDate) } returns remoteList coEvery { examDb.loadAll(1, 1, startDate, realEndDate) } returnsMany listOf( flowOf(remoteList.mapToEntities(semester)), flowOf(remoteList.mapToEntities(semester)) @@ -73,7 +73,7 @@ class ExamRemoteTest { // verify assertEquals(null, res.errorOrNull) assertEquals(2, res.dataOrNull?.size) - coVerify { sdk.getExams(startDate, realEndDate, 1) } + coVerify { sdk.getExams(startDate, realEndDate) } coVerify { examDb.loadAll(1, 1, startDate, realEndDate) } coVerify { examDb.insertAll(match { it.isEmpty() }) } coVerify { examDb.deleteAll(match { it.isEmpty() }) } @@ -82,7 +82,7 @@ class ExamRemoteTest { @Test fun `force refresh with more items in remote`() { // prepare - coEvery { sdk.getExams(startDate, realEndDate, 1) } returns remoteList + coEvery { sdk.getExams(startDate, realEndDate) } returns remoteList coEvery { examDb.loadAll(1, 1, startDate, realEndDate) } returnsMany listOf( flowOf(remoteList.dropLast(1).mapToEntities(semester)), flowOf(remoteList.dropLast(1).mapToEntities(semester)), // after fetch end before save result @@ -97,7 +97,7 @@ class ExamRemoteTest { // verify assertEquals(null, res.errorOrNull) assertEquals(2, res.dataOrNull?.size) - coVerify { sdk.getExams(startDate, realEndDate, 1) } + coVerify { sdk.getExams(startDate, realEndDate) } coVerify { examDb.loadAll(1, 1, startDate, realEndDate) } coVerify { examDb.insertAll(match { @@ -110,7 +110,7 @@ class ExamRemoteTest { @Test fun `force refresh with more items in local`() { // prepare - coEvery { sdk.getExams(startDate, realEndDate, 1) } returns remoteList.dropLast(1) + coEvery { sdk.getExams(startDate, realEndDate) } returns remoteList.dropLast(1) coEvery { examDb.loadAll(1, 1, startDate, realEndDate) } returnsMany listOf( flowOf(remoteList.mapToEntities(semester)), flowOf(remoteList.mapToEntities(semester)), // after fetch end before save result @@ -125,7 +125,7 @@ class ExamRemoteTest { // verify assertEquals(null, res.errorOrNull) assertEquals(1, res.dataOrNull?.size) - coVerify { sdk.getExams(startDate, realEndDate, 1) } + coVerify { sdk.getExams(startDate, realEndDate) } coVerify { examDb.loadAll(1, 1, startDate, realEndDate) } coVerify { examDb.insertAll(match { it.isEmpty() }) } coVerify { @@ -137,7 +137,6 @@ class ExamRemoteTest { private fun getExam(date: LocalDate) = SdkExam( subject = "", - group = "", type = "", description = "", teacher = "", 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 e8d0b6c8f..515b0d66d 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/GradeRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/GradeRepositoryTest.kt @@ -2,6 +2,7 @@ package io.github.wulkanowy.data.repositories import io.github.wulkanowy.data.dataOrNull import io.github.wulkanowy.data.db.dao.GradeDao +import io.github.wulkanowy.data.db.dao.GradeDescriptiveDao import io.github.wulkanowy.data.db.dao.GradeSummaryDao import io.github.wulkanowy.data.errorOrNull import io.github.wulkanowy.data.mappers.mapToEntities @@ -9,13 +10,21 @@ 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.Grades import io.github.wulkanowy.utils.AutoRefreshHelper -import io.mockk.* +import io.mockk.MockKAnnotations +import io.mockk.Runs +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every 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.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import java.time.LocalDate @@ -34,6 +43,9 @@ class GradeRepositoryTest { @MockK private lateinit var gradeSummaryDb: GradeSummaryDao + @MockK + private lateinit var gradeDescriptiveDb: GradeDescriptiveDao + @MockK(relaxUnitFun = true) private lateinit var refreshHelper: AutoRefreshHelper @@ -48,7 +60,8 @@ class GradeRepositoryTest { MockKAnnotations.init(this) every { refreshHelper.shouldBeRefreshed(any()) } returns false - gradeRepository = GradeRepository(gradeDb, gradeSummaryDb, sdk, refreshHelper) + gradeRepository = + GradeRepository(gradeDb, gradeSummaryDb, gradeDescriptiveDb, sdk, refreshHelper) coEvery { gradeDb.deleteAll(any()) } just Runs coEvery { gradeDb.insertAll(any()) } returns listOf() @@ -60,6 +73,13 @@ class GradeRepositoryTest { ) coEvery { gradeSummaryDb.deleteAll(any()) } just Runs coEvery { gradeSummaryDb.insertAll(any()) } returns listOf() + + coEvery { gradeDescriptiveDb.loadAll(any(), any()) } returnsMany listOf( + flowOf(listOf()), + ) + + coEvery { gradeDescriptiveDb.deleteAll(any()) } just Runs + coEvery { gradeDescriptiveDb.insertAll(any()) } returns listOf() } @Test @@ -72,7 +92,7 @@ class GradeRepositoryTest { createGradeApi(5, 4.0, of(2019, 2, 27), "Ocena z dnia logowania"), createGradeApi(5, 4.0, of(2019, 2, 28), "Ocena jeszcze nowsza") ) - coEvery { sdk.getGrades(1) } returns (remoteList to emptyList()) + coEvery { sdk.getGrades(1) } returns createGrades(remoteList) coEvery { gradeDb.loadAll(1, 1) } returnsMany listOf( flowOf(listOf()), // empty because it is new user @@ -122,7 +142,7 @@ class GradeRepositoryTest { ), createGradeApi(2, 5.0, of(2019, 2, 28), "Ta jest już w ogóle nowa") ) - coEvery { sdk.getGrades(1) } returns (remoteList to emptyList()) + coEvery { sdk.getGrades(1) } returns createGrades(remoteList) val localList = listOf( createGradeApi(5, 3.0, of(2019, 2, 25), "Jedna ocena"), @@ -169,7 +189,7 @@ class GradeRepositoryTest { createGradeApi(5, 3.0, of(2019, 2, 25), "Taka sama ocena"), createGradeApi(3, 5.0, of(2019, 2, 26), "Jakaś inna ocena") ) - coEvery { sdk.getGrades(1) } returns (remoteList to emptyList()) + coEvery { sdk.getGrades(1) } returns createGrades(remoteList) val localList = listOf( createGradeApi(5, 3.0, of(2019, 2, 25), "Taka sama ocena"), @@ -200,7 +220,7 @@ class GradeRepositoryTest { createGradeApi(5, 3.0, of(2019, 2, 25), "Taka sama ocena"), // will be added... createGradeApi(3, 5.0, of(2019, 2, 26), "Jakaś inna ocena") ) - coEvery { sdk.getGrades(1) } returns (remoteList to emptyList()) + coEvery { sdk.getGrades(1) } returns createGrades(remoteList) val localList = listOf( createGradeApi(5, 3.0, of(2019, 2, 25), "Taka sama ocena"), @@ -230,7 +250,7 @@ class GradeRepositoryTest { createGradeApi(5, 3.0, of(2019, 2, 25), "Taka sama ocena"), createGradeApi(3, 5.0, of(2019, 2, 26), "Jakaś inna ocena") ) - coEvery { sdk.getGrades(1) } returns (remoteList to emptyList()) + coEvery { sdk.getGrades(1) } returns createGrades(remoteList) coEvery { gradeDb.loadAll(1, 1) } returnsMany listOf( flowOf(listOf()), @@ -250,7 +270,7 @@ class GradeRepositoryTest { fun `force refresh when remote is empty`() { // prepare val remoteList = emptyList() - coEvery { sdk.getGrades(semester.semesterId) } returns (remoteList to emptyList()) + coEvery { sdk.getGrades(semester.semesterId) } returns createGrades(remoteList) val localList = listOf( createGradeApi(5, 3.0, of(2019, 2, 25), "Taka sama ocena"), @@ -284,4 +304,14 @@ class GradeRepositoryTest { weight = weight.toString(), weightValue = weight ) + + private fun createGrades(grades: List): Grades = Grades( + details = grades, + summary = listOf(), + descriptive = emptyList(), + isAverage = false, + isPoints = false, + isForAdults = false, + type = 0, + ) } 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 4efc9c60a..3a18ee979 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 @@ -3,6 +3,7 @@ package io.github.wulkanowy.data.repositories import android.content.Context import io.github.wulkanowy.data.dataOrNull import io.github.wulkanowy.data.db.SharedPrefProvider +import io.github.wulkanowy.data.db.dao.MailboxDao import io.github.wulkanowy.data.db.dao.MessageAttachmentDao import io.github.wulkanowy.data.db.dao.MessagesDao import io.github.wulkanowy.data.db.entities.Message @@ -10,6 +11,7 @@ import io.github.wulkanowy.data.db.entities.MessageWithAttachment import io.github.wulkanowy.data.enums.MessageFolder import io.github.wulkanowy.data.errorOrNull import io.github.wulkanowy.data.toFirstResult +import io.github.wulkanowy.domain.messages.GetMailboxByStudentUseCase import io.github.wulkanowy.getMailboxEntity import io.github.wulkanowy.getStudentEntity import io.github.wulkanowy.sdk.Sdk @@ -55,6 +57,12 @@ class MessageRepositoryTest { @MockK private lateinit var sharedPrefProvider: SharedPrefProvider + @MockK + private lateinit var mailboxDao: MailboxDao + + @MockK + private lateinit var getMailboxByStudentUseCase: GetMailboxByStudentUseCase + private val student = getStudentEntity() private val mailbox = getMailboxEntity() @@ -74,26 +82,33 @@ class MessageRepositoryTest { refreshHelper = refreshHelper, sharedPrefProvider = sharedPrefProvider, json = Json, + mailboxDao = mailboxDao, + getMailboxByStudentUseCase = getMailboxByStudentUseCase, ) } @Test fun `get messages when fetched completely new message without notify`() = runBlocking { - every { messageDb.loadAll(any(), any()) } returns flowOf(emptyList()) + coEvery { mailboxDao.loadAll(any()) } returns listOf(mailbox) + every { messageDb.loadAll(mailbox.globalKey, any()) } returns flowOf(emptyList()) coEvery { sdk.getMessages(Folder.RECEIVED, any()) } returns listOf( - getMessageDto() + getMessageDto().copy( + unreadBy = 5, + readBy = 10, + ) ) coEvery { messageDb.deleteAll(any()) } just Runs coEvery { messageDb.insertAll(any()) } returns listOf() - repository.getMessages( + val res = repository.getMessages( student = student, mailbox = mailbox, folder = MessageFolder.RECEIVED, forceRefresh = true, notify = false, - ).toFirstResult().dataOrNull.orEmpty() + ).toFirstResult() + assertEquals(null, res.errorOrNull) coVerify(exactly = 1) { messageDb.deleteAll(withArg { checkEquals(emptyList()) }) } coVerify { messageDb.insertAll(withArg { @@ -187,13 +202,16 @@ class MessageRepositoryTest { ) = Message( messageGlobalKey = "v4", mailboxKey = "", + email = "", correspondents = "", messageId = messageId, subject = "", date = Instant.EPOCH, folderId = 1, unread = unread, - hasAttachments = false + readBy = 1, + unreadBy = 1, + hasAttachments = false, ).apply { this.content = content } @@ -206,9 +224,11 @@ class MessageRepositoryTest { recipients = listOf(), subject = "", content = "Test", - dateZoned = Instant.EPOCH.atZone(ZoneOffset.UTC), + date = Instant.EPOCH.atZone(ZoneOffset.UTC), folderId = 1, unread = true, + readBy = 1, + unreadBy = 1, 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 6865aa7da..1a3f96795 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 @@ -10,16 +10,21 @@ 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.mockk.* +import io.mockk.MockKAnnotations +import io.mockk.Runs +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every 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 +import java.time.ZoneOffset +import java.time.ZonedDateTime.of class MobileDeviceRepositoryTest { @@ -134,9 +139,7 @@ class MobileDeviceRepositoryTest { id = 0, name = "", deviceId = "", - createDate = 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()) + createDate = of(2019, 5, day, 0, 0, 0, 0, ZoneOffset.UTC), + modificationDate = of(2019, 5, day, 0, 0, 0, 0, ZoneOffset.UTC), ) } diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/SemesterRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/SemesterRepositoryTest.kt index 0ed008851..31098d2ef 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/SemesterRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/SemesterRepositoryTest.kt @@ -15,6 +15,7 @@ import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.SpyK import io.mockk.just import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Assert.assertNotEquals import org.junit.Before @@ -75,21 +76,21 @@ class SemesterRepositoryTest { coEvery { semesterDb.deleteAll(any()) } just Runs coEvery { semesterDb.insertSemesters(any()) } returns listOf() - val items = runBlocking { semesterRepository.getSemesters(student.copy(loginMode = Sdk.Mode.API.name)) } + val items = runBlocking { semesterRepository.getSemesters(student.copy(loginMode = Sdk.Mode.HEBE.name)) } assertEquals(2, items.size) assertEquals(0, items[0].diaryId) } @Test - fun getSemesters_invalidDiary_scrapper() { + fun getSemesters_invalidDiary_scrapper() = runTest { val badSemesters = listOf( - getSemesterPojo(0, 1, now().minusMonths(6), now().minusMonths(3)), - getSemesterPojo(0, 2, now().minusMonths(3), now()) + getSemesterPojo(0, 2, now().minusMonths(6), now()), + getSemesterPojo(0, 2, now(), now().plusMonths(6)), ) val goodSemesters = listOf( - getSemesterPojo(1, 1, now().minusMonths(6), now().minusMonths(3)), - getSemesterPojo(1, 2, now().minusMonths(3), now()) + getSemesterPojo(1, 2, now().minusMonths(6), now()), + getSemesterPojo(2, 3, now(), now().plusMonths(6)), ) coEvery { semesterDb.loadAll(student.studentId, student.classId) } returnsMany listOf( @@ -101,7 +102,9 @@ class SemesterRepositoryTest { coEvery { semesterDb.deleteAll(any()) } just Runs coEvery { semesterDb.insertSemesters(any()) } returns listOf() - val items = runBlocking { semesterRepository.getSemesters(student.copy(loginMode = Sdk.Mode.SCRAPPER.name)) } + val items = semesterRepository.getSemesters( + student = student.copy(loginMode = Sdk.Mode.SCRAPPER.name) + ) assertEquals(2, items.size) assertNotEquals(0, items[0].diaryId) } @@ -188,15 +191,15 @@ class SemesterRepositoryTest { } @Test - fun getSemesters_doubleCurrent_refreshOnNoCurrent() { + fun getSemesters_doubleCurrent_refreshOnNoCurrent() = runTest { val semesters = listOf( - getSemesterEntity(1, 1, now(), now()), - getSemesterEntity(1, 2, now(), now()) + getSemesterEntity(1, 1, now().minusMonths(1), now().plusMonths(1)), + getSemesterEntity(1, 2, now().minusMonths(1), now().plusMonths(1)) ) coEvery { semesterDb.loadAll(student.studentId, student.classId) } returns semesters - val items = runBlocking { semesterRepository.getSemesters(student, refreshOnNoCurrent = true) } + val items = semesterRepository.getSemesters(student, refreshOnNoCurrent = true) assertEquals(2, items.size) } @@ -215,6 +218,7 @@ class SemesterRepositoryTest { @Test(expected = RuntimeException::class) fun getCurrentSemester_emptyList() { coEvery { semesterDb.loadAll(student.studentId, student.classId) } returns emptyList() + coEvery { sdk.getSemesters() } returns emptyList() runBlocking { semesterRepository.getCurrentSemester(student) } } diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/StudentTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/StudentTest.kt deleted file mode 100644 index 9d3d7a2ec..000000000 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/StudentTest.kt +++ /dev/null @@ -1,81 +0,0 @@ -package io.github.wulkanowy.data.repositories - -import io.github.wulkanowy.TestDispatchersProvider -import io.github.wulkanowy.data.db.dao.SemesterDao -import io.github.wulkanowy.data.db.dao.StudentDao -import io.github.wulkanowy.sdk.Sdk -import io.github.wulkanowy.sdk.pojo.Student -import io.github.wulkanowy.utils.AppInfo -import io.mockk.MockKAnnotations -import io.mockk.coEvery -import io.mockk.impl.annotations.MockK -import io.mockk.mockk -import kotlinx.coroutines.runBlocking -import org.junit.Assert.assertEquals -import org.junit.Before -import org.junit.Test - -class StudentTest { - - @MockK - private lateinit var mockSdk: Sdk - - @MockK - private lateinit var studentDb: StudentDao - - @MockK - private lateinit var semesterDb: SemesterDao - - private lateinit var studentRepository: StudentRepository - - @Before - fun initApi() { - MockKAnnotations.init(this) - studentRepository = StudentRepository( - context = mockk(), - dispatchers = TestDispatchersProvider(), - studentDb = studentDb, - semesterDb = semesterDb, - sdk = mockSdk, - appInfo = AppInfo(), - appDatabase = mockk() - ) - } - - @Test - fun testRemoteAll() { - coEvery { mockSdk.getStudentsFromScrapper(any(), any(), any(), any()) } returns listOf( - getStudent("test") - ) - - val students = runBlocking { studentRepository.getStudentsScrapper("", "", "http://fakelog.cf", "") } - assertEquals(1, students.size) - assertEquals("test Kowalski", students.first().student.studentName) - } - - private fun getStudent(name: String): Student { - return Student( - email = "", - symbol = "", - studentId = 0, - userLoginId = 0, - userLogin = "", - userName = "", - studentName = name, - studentSurname = "Kowalski", - schoolSymbol = "", - schoolShortName = "", - schoolName = "", - className = "", - classId = 0, - certificateKey = "", - privateKey = "", - loginMode = Sdk.Mode.SCRAPPER, - mobileBaseUrl = "", - loginType = Sdk.ScrapperLoginType.STANDARD, - scrapperBaseUrl = "", - isParent = false, - semesters = emptyList() - ) - } -} 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 e56aaa5d0..92ad01b18 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 @@ -10,12 +10,17 @@ 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.mockk.* +import io.mockk.MockKAnnotations +import io.mockk.Runs +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.SpyK +import io.mockk.just +import io.mockk.mockk import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals @@ -25,7 +30,7 @@ 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 +import io.github.wulkanowy.sdk.pojo.Lesson as SdkLesson class TimetableRepositoryTest { @@ -62,18 +67,43 @@ class TimetableRepositoryTest { MockKAnnotations.init(this) every { refreshHelper.shouldBeRefreshed(any()) } returns false - timetableRepository = TimetableRepository(timetableDb, timetableAdditionalDao, timetableHeaderDao, sdk, timetableNotificationSchedulerHelper, refreshHelper) + timetableRepository = TimetableRepository( + timetableDb, + timetableAdditionalDao, + timetableHeaderDao, + sdk, + timetableNotificationSchedulerHelper, + refreshHelper + ) } @Test fun `force refresh without difference`() { val remoteList = listOf( - createTimetableRemote(of(2021, 1, 4, 8, 0), 1, "123", "Język polski", "Jan Kowalski", false), - createTimetableRemote(of(2021, 1, 4, 8, 50), 2, "124", "Język niemiecki", "Joanna Czarniecka", true) + createTimetableRemote( + start = of(2021, 1, 4, 8, 0), + number = 1, + room = "123", + subject = "Język polski", + teacher = "Jan Kowalski", + changes = false + ), + createTimetableRemote( + start = of(2021, 1, 4, 8, 50), + number = 2, + room = "124", + subject = "Język niemiecki", + teacher = "Joanna Czarniecka", + changes = true + ) ) // prepare - coEvery { sdk.getTimetableFull(startDate, endDate) } returns TimetableFull(emptyList(), remoteList, emptyList()) + coEvery { sdk.getTimetable(startDate, endDate) } returns mockk { + every { headers } returns emptyList() + every { lessons } returns remoteList + every { additional } returns emptyList() + } coEvery { timetableDb.loadAll(1, 1, startDate, endDate) } returnsMany listOf( flowOf(remoteList.mapToEntities(semester)), flowOf(remoteList.mapToEntities(semester)) @@ -81,7 +111,14 @@ class TimetableRepositoryTest { coEvery { timetableDb.insertAll(any()) } returns listOf(1, 2, 3) coEvery { timetableDb.deleteAll(any()) } just Runs - coEvery { timetableAdditionalDao.loadAll(1, 1, startDate, endDate) } returns flowOf(listOf()) + coEvery { + timetableAdditionalDao.loadAll( + diaryId = 1, + studentId = 1, + from = startDate, + end = endDate + ) + } returns flowOf(listOf()) coEvery { timetableAdditionalDao.deleteAll(emptyList()) } just Runs coEvery { timetableAdditionalDao.insertAll(emptyList()) } returns listOf(1, 2, 3) @@ -90,23 +127,36 @@ class TimetableRepositoryTest { coEvery { timetableHeaderDao.deleteAll(emptyList()) } just Runs // execute - val res = runBlocking { timetableRepository.getTimetable(student, semester, startDate, endDate, true).toFirstResult() } + val res = runBlocking { + timetableRepository.getTimetable( + student = student, + semester = semester, + start = startDate, + end = endDate, + forceRefresh = true + ).toFirstResult() + } // verify assertEquals(null, res.errorOrNull) assertEquals(2, res.dataOrNull!!.lessons.size) - coVerify { sdk.getTimetableFull(startDate, endDate) } + coVerify { sdk.getTimetable(startDate, endDate) } coVerify { timetableDb.loadAll(1, 1, startDate, endDate) } coVerify { timetableDb.insertAll(match { it.isEmpty() }) } coVerify { timetableDb.deleteAll(match { it.isEmpty() }) } } - private fun createTimetableRemote(start: LocalDateTime, number: Int = 1, room: String = "", subject: String = "", teacher: String = "", changes: Boolean = false) = SdkTimetable( + private fun createTimetableRemote( + start: LocalDateTime, + number: Int = 1, + room: String = "", + subject: String = "", + teacher: String = "", + changes: Boolean = false + ) = SdkLesson( number = number, - start = start, - end = start.plusMinutes(45), - startZoned = start.atZone(ZoneId.systemDefault()), - endZoned = start.plusMinutes(45).atZone(ZoneId.systemDefault()), + start = start.atZone(ZoneId.systemDefault()), + end = start.plusMinutes(45).atZone(ZoneId.systemDefault()), date = start.toLocalDate(), subject = subject, group = "", diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/MailboxRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/domain/GetMailboxByStudentUseCaseTest.kt similarity index 67% rename from app/src/test/java/io/github/wulkanowy/data/repositories/MailboxRepositoryTest.kt rename to app/src/test/java/io/github/wulkanowy/domain/GetMailboxByStudentUseCaseTest.kt index 9198560fe..34a8fe991 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/MailboxRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/domain/GetMailboxByStudentUseCaseTest.kt @@ -1,65 +1,52 @@ -package io.github.wulkanowy.data.repositories +package io.github.wulkanowy.domain 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.domain.messages.GetMailboxByStudentUseCase 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 +import kotlin.test.assertEquals +import kotlin.test.assertNull @OptIn(ExperimentalCoroutinesApi::class) -class MailboxRepositoryTest { - - @SpyK - private var sdk = Sdk() +class GetMailboxByStudentUseCaseTest { @MockK private lateinit var mailboxDao: MailboxDao - @MockK - private lateinit var refreshHelper: AutoRefreshHelper - - private lateinit var systemUnderTest: MailboxRepository + private lateinit var systemUnderTest: GetMailboxByStudentUseCase @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, - ) + systemUnderTest = GetMailboxByStudentUseCase(mailboxDao = mailboxDao) } - @Test(expected = IllegalArgumentException::class) + @Test fun `get mailbox that doesn't exist`() = runTest { val student = getStudentEntity( userName = "Stanisław Kowalski", studentName = "Jan Kowalski", ) - coEvery { sdk.getMailboxes() } returns emptyList() + coEvery { mailboxDao.loadAll(any()) } returns emptyList() - systemUnderTest.getMailbox(student) + assertNull(systemUnderTest(student)) } @Test @@ -73,10 +60,22 @@ class MailboxRepositoryTest { expectedMailbox, ) - val selectedMailbox = systemUnderTest.getMailbox(student) + val selectedMailbox = systemUnderTest(student) assertEquals(expectedMailbox, selectedMailbox) } + @Test + fun `get mailbox for user with reversed name`() = runTest { + val student = getStudentEntity( + userName = "Kowalski Jan", + studentName = "Jan Kowalski", + ) + val expectedMailbox = getMailboxEntity("Kowalski Jan") + coEvery { mailboxDao.loadAll(any()) } returns listOf(expectedMailbox) + + assertEquals(expectedMailbox, systemUnderTest(student)) + } + @Test fun `get mailbox for unique non-authorized student`() = runTest { val student = getStudentEntity( @@ -88,7 +87,7 @@ class MailboxRepositoryTest { expectedMailbox, ) - assertEquals(expectedMailbox, systemUnderTest.getMailbox(student)) + assertEquals(expectedMailbox, systemUnderTest(student)) } @Test @@ -102,10 +101,10 @@ class MailboxRepositoryTest { expectedMailbox, ) - assertEquals(expectedMailbox, systemUnderTest.getMailbox(student)) + assertEquals(expectedMailbox, systemUnderTest(student)) } - @Test(expected = IllegalArgumentException::class) + @Test fun `get mailbox for not-unique non-authorized student`() = runTest { val student = getStudentEntity( userName = "Stanisław Kowalski", @@ -116,7 +115,7 @@ class MailboxRepositoryTest { getMailboxEntity("Jan Kurowski"), ) - systemUnderTest.getMailbox(student) + assertNull(systemUnderTest(student)) } @Test @@ -130,7 +129,7 @@ class MailboxRepositoryTest { expectedMailbox, ) - assertEquals(expectedMailbox, systemUnderTest.getMailbox(student)) + assertEquals(expectedMailbox, systemUnderTest(student)) } @Test @@ -144,7 +143,7 @@ class MailboxRepositoryTest { expectedMailbox, ) - assertEquals(expectedMailbox, systemUnderTest.getMailbox(student)) + assertEquals(expectedMailbox, systemUnderTest(student)) } @Test @@ -158,40 +157,65 @@ class MailboxRepositoryTest { expectedMailbox, ) - assertEquals(expectedMailbox, systemUnderTest.getMailbox(student)) + assertEquals(expectedMailbox, systemUnderTest(student)) + } + + @Test + fun `get mailbox for student with mailboxes from two different schools`() = runTest { + val student = getStudentEntity( + userName = "Kamil Bednarek", + studentName = "Kamil Bednarek", + schoolShortName = "CKZiU", + ) + val mailbox1 = getMailboxEntity( + studentName = "Kamil Bednarek", + schoolShortName = "ZSTiO", + ) + val mailbox2 = getMailboxEntity( + studentName = "Kamil Bednarek", + schoolShortName = "CKZiU", + ) + coEvery { mailboxDao.loadAll(any()) } returns listOf(mailbox1, mailbox2) + + assertEquals(mailbox2, systemUnderTest(student)) } private fun getMailboxEntity( studentName: String, + schoolShortName: String = "test", ) = Mailbox( globalKey = "", fullName = "", userName = "", - userLoginId = 123, + email = "", + schoolId = "", + symbol = "", studentName = studentName, - schoolNameShort = "", + schoolNameShort = schoolShortName, type = MailboxType.STUDENT, ) private fun getStudentEntity( studentName: String, userName: String, + schoolShortName: String = "test", ) = Student( scrapperBaseUrl = "http://fakelog.cf", + scrapperDomainSuffix = "", email = "jan@fakelog.cf", certificateKey = "", classId = 0, className = "", isCurrent = false, isParent = false, - loginMode = Sdk.Mode.API.name, + loginMode = Sdk.Mode.HEBE.name, loginType = Sdk.ScrapperLoginType.STANDARD.name, mobileBaseUrl = "", password = "", privateKey = "", registrationDate = Instant.now(), schoolName = "", - schoolShortName = "test", + schoolShortName = schoolShortName, schoolSymbol = "", studentId = 1, studentName = studentName, 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 a6ecdc26b..6a717f6f6 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 @@ -5,10 +5,12 @@ import io.github.wulkanowy.data.dataOrNull import io.github.wulkanowy.data.db.entities.Grade import io.github.wulkanowy.data.db.entities.GradeSummary import io.github.wulkanowy.data.db.entities.Student +import io.github.wulkanowy.data.errorOrNull import io.github.wulkanowy.data.repositories.GradeRepository import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.SemesterRepository import io.github.wulkanowy.data.resourceFlow +import io.github.wulkanowy.data.toFirstResult import io.github.wulkanowy.getSemesterEntity import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.Status @@ -20,6 +22,7 @@ import io.mockk.impl.annotations.MockK import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.toList import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals @@ -31,7 +34,9 @@ import java.time.LocalDate.of class GradeAverageProviderTest { - private suspend fun Flow>.getResult() = toList()[1].dataOrNull!! + private suspend fun Flow>.getResult() = toFirstResult().let { + it.dataOrNull ?: throw it.errorOrNull ?: error("Unknown state") + } @MockK lateinit var preferencesRepository: PreferencesRepository @@ -46,6 +51,7 @@ class GradeAverageProviderTest { private val student = Student( scrapperBaseUrl = "", + scrapperDomainSuffix = "", mobileBaseUrl = "", loginType = "", loginMode = "SCRAPPER", @@ -133,63 +139,113 @@ class GradeAverageProviderTest { fun setUp() { MockKAnnotations.init(this) - every { preferencesRepository.gradeAverageForceCalc } returns false + every { preferencesRepository.gradeAverageForceCalcFlow } returns flowOf(false) coEvery { semesterRepository.getSemesters(student) } returns semesters - every { preferencesRepository.gradeMinusModifier } returns .33 - every { preferencesRepository.gradePlusModifier } returns .33 + every { preferencesRepository.gradeMinusModifierFlow } returns flowOf(.33) + every { preferencesRepository.gradePlusModifierFlow } returns flowOf(.33) + every { preferencesRepository.gradeAverageModeFlow } returns flowOf(GradeAverageMode.ONE_SEMESTER) + every { preferencesRepository.gradeAverageForceCalcFlow } returns flowOf(false) + every { preferencesRepository.isOptionalArithmeticAverageFlow } returns flowOf(false) - gradeAverageProvider = GradeAverageProvider(semesterRepository, gradeRepository, preferencesRepository) + gradeAverageProvider = + GradeAverageProvider(semesterRepository, gradeRepository, preferencesRepository) } @Test fun `calc current semester standard average with no weights`() { - every { preferencesRepository.gradeAverageForceCalc } returns false - every { preferencesRepository.isOptionalArithmeticAverage } returns false - every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.ONE_SEMESTER + every { preferencesRepository.gradeAverageForceCalcFlow } returns flowOf(false) + every { preferencesRepository.isOptionalArithmeticAverageFlow } returns flowOf(false) + every { preferencesRepository.gradeAverageModeFlow } returns flowOf(GradeAverageMode.ONE_SEMESTER) coEvery { gradeRepository.getGrades( student, semesters[2], true ) - } returns resourceFlow { noWeightGrades to noWeightGradesSummary } + } returns resourceFlow { + Triple(noWeightGrades, noWeightGradesSummary, 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.0, items.single { it.subject == "Matematyka" }.average, .0) // from summary: 0,0 + assertEquals( + 0.0, + items.single { it.subject == "Matematyka" }.average, + .0 + ) // from summary: 0,0 } @Test fun `calc current semester arithmetic average with no weights`() { - every { preferencesRepository.gradeAverageForceCalc } returns false - every { preferencesRepository.isOptionalArithmeticAverage } returns true - every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.ONE_SEMESTER + every { preferencesRepository.gradeAverageForceCalcFlow } returns flowOf(false) + every { preferencesRepository.isOptionalArithmeticAverageFlow } returns flowOf(true) + every { preferencesRepository.gradeAverageModeFlow } returns flowOf(GradeAverageMode.ONE_SEMESTER) coEvery { gradeRepository.getGrades( student, semesters[2], true ) - } returns resourceFlow { noWeightGrades to noWeightGradesArithmeticSummary } + } returns resourceFlow { + Triple(noWeightGrades, noWeightGradesArithmeticSummary, emptyList()) + } - val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId, true).getResult() } + val items = runBlocking { + gradeAverageProvider.getGradesDetailsWithAverage( + student, + semesters[2].semesterId, + true + ).getResult() + } - assertEquals(4.0, items.single { it.subject == "Matematyka" }.average, .0) // from summary: 4,0 + assertEquals( + 4.0, + items.single { it.subject == "Matematyka" }.average, + .0 + ) // from summary: 4,0 } @Test fun `calc current semester average with load from cache sequence`() { - every { preferencesRepository.gradeAverageForceCalc } returns true - every { preferencesRepository.isOptionalArithmeticAverage } returns false - every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.ONE_SEMESTER + every { preferencesRepository.gradeAverageForceCalcFlow } returns flowOf(true) + every { preferencesRepository.isOptionalArithmeticAverageFlow } returns flowOf(false) + every { preferencesRepository.gradeAverageModeFlow } returns flowOf(GradeAverageMode.ONE_SEMESTER) coEvery { semesterRepository.getSemesters(student) } returns semesters coEvery { gradeRepository.getGrades(student, semesters[2], true) } returns flow { emit(Resource.Loading()) - emit(Resource.Intermediate(secondGradeWithModifier to secondSummariesWithModifier)) - emit(Resource.Success(secondGradeWithModifier to secondSummariesWithModifier)) + emit( + Resource.Intermediate( + Triple( + secondGradeWithModifier, + secondSummariesWithModifier, + emptyList() + ) + ) + ) + emit( + Resource.Success( + Triple( + secondGradeWithModifier, + secondSummariesWithModifier, + emptyList() + ) + ) + ) } - val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId, true).toList() } + val items = runBlocking { + gradeAverageProvider.getGradesDetailsWithAverage( + student, + semesters[2].semesterId, + true + ).toList() + } with(items[0]) { assertEquals(Status.LOADING, status) @@ -204,27 +260,53 @@ class GradeAverageProviderTest { assertEquals(1, dataOrNull?.size) } - assertEquals(3.5, items[1].dataOrNull?.single { it.subject == "Język polski" }?.average ?: 0.0, .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 - fun `calc all year semester average with delayed emit`(){ - every { preferencesRepository.gradeAverageForceCalc } returns true - every { preferencesRepository.isOptionalArithmeticAverage } returns false - every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.ALL_YEAR + fun `calc all year semester average with delayed emit`() { + every { preferencesRepository.gradeAverageForceCalcFlow } returns flowOf(true) + every { preferencesRepository.isOptionalArithmeticAverageFlow } returns flowOf(false) + every { preferencesRepository.gradeAverageModeFlow } returns flowOf(GradeAverageMode.ALL_YEAR) coEvery { semesterRepository.getSemesters(student) } returns semesters coEvery { gradeRepository.getGrades(student, semesters[2], false) } returns flow { emit(Resource.Loading()) delay(1000) - emit(Resource.Success(secondGradeWithModifier to secondSummariesWithModifier)) + emit( + Resource.Success( + Triple( + secondGradeWithModifier, + secondSummariesWithModifier, + emptyList() + ) + ) + ) } coEvery { gradeRepository.getGrades(student, semesters[1], false) } returns flow { emit(Resource.Loading()) - emit(Resource.Success(secondGradeWithModifier to secondSummariesWithModifier)) + emit( + Resource.Success( + Triple( + secondGradeWithModifier, + secondSummariesWithModifier, + emptyList() + ) + ) + ) } - val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId, false).toList() } + val items = runBlocking { + gradeAverageProvider.getGradesDetailsWithAverage( + student, + semesters[2].semesterId, + false + ).toList() + } with(items[0]) { assertEquals(Status.LOADING, status) @@ -235,14 +317,18 @@ class GradeAverageProviderTest { assertEquals(1, dataOrNull?.size) } - assertEquals(3.5, items[1].dataOrNull?.single { it.subject == "Język polski" }?.average ?: 0.0, .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 fun `calc both semesters average with grade without grade in second semester`() { - every { preferencesRepository.gradeAverageForceCalc } returns true - every { preferencesRepository.isOptionalArithmeticAverage } returns false - every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.BOTH_SEMESTERS + every { preferencesRepository.gradeAverageForceCalcFlow } returns flowOf(true) + every { preferencesRepository.isOptionalArithmeticAverageFlow } returns flowOf(false) + every { preferencesRepository.gradeAverageModeFlow } returns flowOf(GradeAverageMode.BOTH_SEMESTERS) coEvery { gradeRepository.getGrades( @@ -250,10 +336,24 @@ class GradeAverageProviderTest { 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) + } returns resourceFlow { + Triple( + secondGradeWithModifier, + secondSummariesWithModifier, + emptyList() + ) + } + coEvery { + gradeRepository.getGrades( + student, + semesters[2], + false + ) + } returns resourceFlow { + Triple( + listOf(getGrade(semesters[2].semesterId, "Język polski", .0, .0, .0)), + listOf(getSummary(semesters[2].semesterId, "Język polski", 2.5)), + emptyList() ) } @@ -270,9 +370,9 @@ class GradeAverageProviderTest { @Test fun `calc both semesters average with no grade in second semester but with average in first semester`() { - every { preferencesRepository.gradeAverageForceCalc } returns false - every { preferencesRepository.isOptionalArithmeticAverage } returns false - every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.BOTH_SEMESTERS + every { preferencesRepository.gradeAverageForceCalcFlow } returns flowOf(false) + every { preferencesRepository.isOptionalArithmeticAverageFlow } returns flowOf(false) + every { preferencesRepository.gradeAverageModeFlow } returns flowOf(GradeAverageMode.BOTH_SEMESTERS) coEvery { gradeRepository.getGrades( @@ -280,14 +380,30 @@ class GradeAverageProviderTest { semesters[1], false ) - } returns resourceFlow { secondGradeWithModifier to secondSummariesWithModifier } + } returns resourceFlow { + Triple( + secondGradeWithModifier, + secondSummariesWithModifier, + emptyList() + ) + } coEvery { gradeRepository.getGrades( student, semesters[2], false ) - } returns resourceFlow { emptyList() to listOf(getSummary(24, "Język polski", .0)) } + } returns resourceFlow { + Triple( + emptyList(), listOf( + getSummary( + 24, + "Język polski", + .0 + ) + ), emptyList() + ) + } val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage( @@ -302,9 +418,9 @@ class GradeAverageProviderTest { @Test fun `force calc average on no grades`() { - every { preferencesRepository.gradeAverageForceCalc } returns true - every { preferencesRepository.isOptionalArithmeticAverage } returns false - every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.BOTH_SEMESTERS + every { preferencesRepository.gradeAverageForceCalcFlow } returns flowOf(true) + every { preferencesRepository.isOptionalArithmeticAverageFlow } returns flowOf(false) + every { preferencesRepository.gradeAverageModeFlow } returns flowOf(GradeAverageMode.BOTH_SEMESTERS) coEvery { gradeRepository.getGrades( @@ -312,14 +428,22 @@ class GradeAverageProviderTest { semesters[1], true ) - } returns resourceFlow { emptyList() to emptyList() } + } returns resourceFlow { + Triple( + emptyList(), + emptyList(), + emptyList() + ) + } coEvery { gradeRepository.getGrades( student, semesters[2], true ) - } returns resourceFlow { emptyList() to emptyList() } + } returns resourceFlow { + Triple(emptyList(), emptyList(), emptyList()) + } val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage( @@ -334,9 +458,9 @@ class GradeAverageProviderTest { @Test fun `force calc current semester average with default modifiers in scraper mode`() { - every { preferencesRepository.gradeAverageForceCalc } returns true - every { preferencesRepository.isOptionalArithmeticAverage } returns false - every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.ONE_SEMESTER + every { preferencesRepository.gradeAverageForceCalcFlow } returns flowOf(true) + every { preferencesRepository.isOptionalArithmeticAverageFlow } returns flowOf(false) + every { preferencesRepository.gradeAverageModeFlow } returns flowOf(GradeAverageMode.ONE_SEMESTER) coEvery { semesterRepository.getSemesters(student) } returns semesters coEvery { gradeRepository.getGrades( @@ -344,23 +468,39 @@ class GradeAverageProviderTest { semesters[2], true ) - } returns resourceFlow { secondGradeWithModifier to secondSummariesWithModifier } + } returns resourceFlow { + Triple( + secondGradeWithModifier, + secondSummariesWithModifier, + emptyList() + ) + } - val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId, true).getResult() } + val items = runBlocking { + gradeAverageProvider.getGradesDetailsWithAverage( + student, + semesters[2].semesterId, + true + ).getResult() + } - assertEquals(3.5, items.single { it.subject == "Język polski" }.average, .0) // from details and after set custom plus/minus + assertEquals( + 3.5, + items.single { it.subject == "Język polski" }.average, + .0 + ) // from details and after set custom plus/minus } @Test fun `force calc current semester average with custom modifiers in scraper mode`() { val student = student.copy(loginMode = Sdk.Mode.SCRAPPER.name) - every { preferencesRepository.gradeAverageForceCalc } returns true - every { preferencesRepository.isOptionalArithmeticAverage } returns false - every { preferencesRepository.gradeMinusModifier } returns .33 - every { preferencesRepository.gradePlusModifier } returns .33 + every { preferencesRepository.gradeAverageForceCalcFlow } returns flowOf(true) + every { preferencesRepository.isOptionalArithmeticAverageFlow } returns flowOf(false) + every { preferencesRepository.gradeMinusModifierFlow } returns flowOf(.33) + every { preferencesRepository.gradePlusModifierFlow } returns flowOf(.33) - every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.ONE_SEMESTER + every { preferencesRepository.gradeAverageModeFlow } returns flowOf(GradeAverageMode.ONE_SEMESTER) coEvery { semesterRepository.getSemesters(student) } returns semesters coEvery { gradeRepository.getGrades( @@ -368,23 +508,39 @@ class GradeAverageProviderTest { semesters[2], true ) - } returns resourceFlow { secondGradeWithModifier to secondSummariesWithModifier } + } returns resourceFlow { + Triple( + secondGradeWithModifier, + secondSummariesWithModifier, + emptyList() + ) + } - val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId, true).getResult() } + val items = runBlocking { + gradeAverageProvider.getGradesDetailsWithAverage( + student, + semesters[2].semesterId, + true + ).getResult() + } - assertEquals(3.5, items.single { it.subject == "Język polski" }.average, .0) // from details and after set custom plus/minus + assertEquals( + 3.5, + items.single { it.subject == "Język polski" }.average, + .0 + ) // from details and after set custom plus/minus } @Test fun `force calc current semester average with custom modifiers in api mode`() { - val student = student.copy(loginMode = Sdk.Mode.API.name) + val student = student.copy(loginMode = Sdk.Mode.HEBE.name) - every { preferencesRepository.gradeAverageForceCalc } returns true - every { preferencesRepository.isOptionalArithmeticAverage } returns false - every { preferencesRepository.gradeMinusModifier } returns .33 // useless in this mode - every { preferencesRepository.gradePlusModifier } returns .33 + every { preferencesRepository.gradeAverageForceCalcFlow } returns flowOf(true) + every { preferencesRepository.isOptionalArithmeticAverageFlow } returns flowOf(false) + every { preferencesRepository.gradeMinusModifierFlow } returns flowOf(.33) // useless in this mode + every { preferencesRepository.gradePlusModifierFlow } returns flowOf(.33) - every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.ONE_SEMESTER + every { preferencesRepository.gradeAverageModeFlow } returns flowOf(GradeAverageMode.ONE_SEMESTER) coEvery { semesterRepository.getSemesters(student) } returns semesters coEvery { gradeRepository.getGrades( @@ -392,23 +548,39 @@ class GradeAverageProviderTest { semesters[2], true ) - } returns resourceFlow { secondGradeWithModifier to secondSummariesWithModifier } + } returns resourceFlow { + Triple( + secondGradeWithModifier, + secondSummariesWithModifier, + emptyList() + ) + } - val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId, true).getResult() } + val items = runBlocking { + gradeAverageProvider.getGradesDetailsWithAverage( + student, + semesters[2].semesterId, + true + ).getResult() + } - assertEquals(3.375, items.single { it.subject == "Język polski" }.average, .0) // (from details): 3.375 + assertEquals( + 3.375, + items.single { it.subject == "Język polski" }.average, + .0 + ) // (from details): 3.375 } @Test fun `force calc current semester average with custom modifiers in hybrid mode`() { val student = student.copy(loginMode = Sdk.Mode.HYBRID.name) - every { preferencesRepository.gradeAverageForceCalc } returns true - every { preferencesRepository.isOptionalArithmeticAverage } returns false - every { preferencesRepository.gradeMinusModifier } returns .33 // useless in this mode - every { preferencesRepository.gradePlusModifier } returns .33 + every { preferencesRepository.gradeAverageForceCalcFlow } returns flowOf(true) + every { preferencesRepository.isOptionalArithmeticAverageFlow } returns flowOf(false) + every { preferencesRepository.gradeMinusModifierFlow } returns flowOf(.33) // useless in this mode + every { preferencesRepository.gradePlusModifierFlow } returns flowOf(.33) - every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.ONE_SEMESTER + every { preferencesRepository.gradeAverageModeFlow } returns flowOf(GradeAverageMode.ONE_SEMESTER) coEvery { semesterRepository.getSemesters(student) } returns semesters coEvery { gradeRepository.getGrades( @@ -416,86 +588,142 @@ class GradeAverageProviderTest { semesters[2], true ) - } returns resourceFlow { secondGradeWithModifier to secondSummariesWithModifier } + } returns resourceFlow { + Triple( + secondGradeWithModifier, + secondSummariesWithModifier, + emptyList() + ) + } - val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId, true).getResult() } + val items = runBlocking { + gradeAverageProvider.getGradesDetailsWithAverage( + student, + semesters[2].semesterId, + true + ).getResult() + } - assertEquals(3.375, items.single { it.subject == "Język polski" }.average, .0) // (from details): 3.375 + assertEquals( + 3.375, + items.single { it.subject == "Język polski" }.average, + .0 + ) // (from details): 3.375 } @Test fun `calc current semester average`() { - every { preferencesRepository.gradeAverageForceCalc } returns false - every { preferencesRepository.isOptionalArithmeticAverage } returns false - every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.ONE_SEMESTER + every { preferencesRepository.gradeAverageForceCalcFlow } returns flowOf(false) + every { preferencesRepository.isOptionalArithmeticAverageFlow } returns flowOf(false) + every { preferencesRepository.gradeAverageModeFlow } returns flowOf(GradeAverageMode.ONE_SEMESTER) coEvery { gradeRepository.getGrades( student, semesters[2], true ) - } returns resourceFlow { secondGrades to secondSummaries } + } returns resourceFlow { Triple(secondGrades, secondSummaries, 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(2.9, items.single { it.subject == "Matematyka" }.average, .0) // from summary: 2,9 + assertEquals( + 2.9, + items.single { it.subject == "Matematyka" }.average, + .0 + ) // from summary: 2,9 assertEquals(3.4, items.single { it.subject == "Fizyka" }.average, .0) // from details: 3,4 } @Test fun `force calc current semester average`() { - every { preferencesRepository.gradeAverageForceCalc } returns true - every { preferencesRepository.isOptionalArithmeticAverage } returns false - every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.ONE_SEMESTER + every { preferencesRepository.gradeAverageForceCalcFlow } returns flowOf(true) + every { preferencesRepository.isOptionalArithmeticAverageFlow } returns flowOf(false) + every { preferencesRepository.gradeAverageModeFlow } returns flowOf(GradeAverageMode.ONE_SEMESTER) coEvery { gradeRepository.getGrades( student, semesters[2], true ) - } returns resourceFlow { secondGrades to secondSummaries } + } returns resourceFlow { Triple(secondGrades, secondSummaries, 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(2.5, items.single { it.subject == "Matematyka" }.average, .0) // from details: 2,5 + assertEquals( + 2.5, + items.single { it.subject == "Matematyka" }.average, + .0 + ) // from details: 2,5 assertEquals(3.0, items.single { it.subject == "Fizyka" }.average, .0) // from details: 3,0 } @Test fun `force calc full year average when current is first`() { - every { preferencesRepository.gradeAverageForceCalc } returns true - every { preferencesRepository.isOptionalArithmeticAverage } returns false - every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.ALL_YEAR + every { preferencesRepository.gradeAverageForceCalcFlow } returns flowOf(true) + every { preferencesRepository.isOptionalArithmeticAverageFlow } returns flowOf(false) + every { preferencesRepository.gradeAverageModeFlow } returns flowOf(GradeAverageMode.ALL_YEAR) coEvery { gradeRepository.getGrades( student, semesters[1], true ) - } returns resourceFlow { firstGrades to firstSummaries } + } returns resourceFlow { Triple(firstGrades, firstSummaries, emptyList()) } - val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[1].semesterId, true).getResult() } + val items = runBlocking { + gradeAverageProvider.getGradesDetailsWithAverage( + student, + semesters[1].semesterId, + true + ).getResult() + } assertEquals(2, items.size) - assertEquals(3.5, items.single { it.subject == "Matematyka" }.average, .0) // (from summary): 3,5 - assertEquals(3.5, items.single { it.subject == "Fizyka" }.average, .0) // (from summary): 3,5 + assertEquals( + 3.5, + items.single { it.subject == "Matematyka" }.average, + .0 + ) // (from summary): 3,5 + assertEquals( + 3.5, + items.single { it.subject == "Fizyka" }.average, + .0 + ) // (from summary): 3,5 } @Test fun `calc full year average when current is first with load from cache sequence`() { - every { preferencesRepository.gradeAverageForceCalc } returns true - every { preferencesRepository.isOptionalArithmeticAverage } returns false - every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.ALL_YEAR + every { preferencesRepository.gradeAverageForceCalcFlow } returns flowOf(true) + every { preferencesRepository.isOptionalArithmeticAverageFlow } returns flowOf(false) + every { preferencesRepository.gradeAverageModeFlow } returns flowOf(GradeAverageMode.ALL_YEAR) coEvery { semesterRepository.getSemesters(student) } returns semesters coEvery { gradeRepository.getGrades(student, semesters[1], true) } returns flow { emit(Resource.Loading()) - emit(Resource.Intermediate(firstGrades to firstSummaries)) - emit(Resource.Success(firstGrades to firstSummaries)) + emit(Resource.Intermediate(Triple(firstGrades, firstSummaries, emptyList()))) + emit(Resource.Success(Triple(firstGrades, firstSummaries, emptyList()))) } - val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[1].semesterId, true).toList() } + val items = runBlocking { + gradeAverageProvider.getGradesDetailsWithAverage( + student, + semesters[1].semesterId, + true + ).toList() + } with(items[0]) { assertEquals(Status.LOADING, status) @@ -511,55 +739,99 @@ class GradeAverageProviderTest { } 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 + 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 fun `calc both semesters average`() { - 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 resourceFlow { - firstGrades to listOf( - getSummary(22, "Matematyka", 3.0), - getSummary(22, "Fizyka", 3.5) + every { preferencesRepository.gradeAverageModeFlow } returns flowOf(GradeAverageMode.BOTH_SEMESTERS) + every { preferencesRepository.gradeAverageForceCalcFlow } returns flowOf(false) + every { preferencesRepository.isOptionalArithmeticAverageFlow } returns flowOf(false) + coEvery { + gradeRepository.getGrades( + student, + semesters[1], + true + ) + } returns resourceFlow { + Triple( + firstGrades, listOf( + getSummary(22, "Matematyka", 3.0), + getSummary(22, "Fizyka", 3.5) + ), emptyList() ) } - coEvery { gradeRepository.getGrades(student, semesters[2], true) } returns resourceFlow { - secondGrades to listOf( - getSummary(22, "Matematyka", 3.5), - getSummary(22, "Fizyka", 4.0) + coEvery { + gradeRepository.getGrades( + student, + semesters[2], + true + ) + } returns resourceFlow { + Triple( + secondGrades, + listOf( + getSummary(22, "Matematyka", 3.5), + getSummary(22, "Fizyka", 4.0) + ), + 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.25, items.single { it.subject == "Matematyka" }.average, .0) // (from summaries ↑): 3,0 + 3,5 → 3,25 - assertEquals(3.75, items.single { it.subject == "Fizyka" }.average, .0) // (from summaries ↑): 3,5 + 4,0 → 3,75 + assertEquals( + 3.25, + items.single { it.subject == "Matematyka" }.average, + .0 + ) // (from summaries ↑): 3,0 + 3,5 → 3,25 + assertEquals( + 3.75, + items.single { it.subject == "Fizyka" }.average, + .0 + ) // (from summaries ↑): 3,5 + 4,0 → 3,75 } @Test fun `calc both semesters average when current is second with load from cache sequence`() { - every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.BOTH_SEMESTERS - every { preferencesRepository.isOptionalArithmeticAverage } returns false - every { preferencesRepository.gradeAverageForceCalc } returns false + every { preferencesRepository.gradeAverageModeFlow } returns flowOf(GradeAverageMode.BOTH_SEMESTERS) + every { preferencesRepository.isOptionalArithmeticAverageFlow } returns flowOf(false) + every { preferencesRepository.gradeAverageForceCalcFlow } returns flowOf(false) coEvery { gradeRepository.getGrades(student, semesters[1], true) } returns flow { emit(Resource.Loading()) emit( Resource.Intermediate( - firstGrades to listOf( - getSummary(22, "Matematyka", 3.0), - getSummary(22, "Fizyka", 3.5) + Triple( + firstGrades, listOf( + getSummary(22, "Matematyka", 3.0), + getSummary(22, "Fizyka", 3.5) + ), emptyList() ) ) ) emit( Resource.Success( - firstGrades to listOf( - getSummary(22, "Matematyka", 3.0), - getSummary(22, "Fizyka", 3.5) + Triple( + firstGrades, listOf( + getSummary(22, "Matematyka", 3.0), + getSummary(22, "Fizyka", 3.5) + ), emptyList() ) ) ) @@ -568,23 +840,33 @@ class GradeAverageProviderTest { emit(Resource.Loading()) emit( Resource.Intermediate( - secondGrades to listOf( - getSummary(22, "Matematyka", 3.5), - getSummary(22, "Fizyka", 4.0) + Triple( + secondGrades, listOf( + getSummary(22, "Matematyka", 3.5), + getSummary(22, "Fizyka", 4.0) + ), emptyList() ) ) ) emit( Resource.Success( - secondGrades to listOf( - getSummary(22, "Matematyka", 3.5), - getSummary(22, "Fizyka", 4.0) + Triple( + secondGrades, listOf( + getSummary(22, "Matematyka", 3.5), + getSummary(22, "Fizyka", 4.0) + ), emptyList() ) ) ) } - val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId, true).toList() } + val items = runBlocking { + gradeAverageProvider.getGradesDetailsWithAverage( + student, + semesters[2].semesterId, + true + ).toList() + } with(items[0]) { assertEquals(Status.LOADING, status) @@ -600,26 +882,42 @@ class GradeAverageProviderTest { } 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 + 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 fun `force calc full year average`() { - every { preferencesRepository.gradeAverageForceCalc } returns true - every { preferencesRepository.isOptionalArithmeticAverage } returns false - every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.ALL_YEAR + every { preferencesRepository.gradeAverageForceCalcFlow } returns flowOf(true) + every { preferencesRepository.isOptionalArithmeticAverageFlow } returns flowOf(false) + every { preferencesRepository.gradeAverageModeFlow } returns flowOf(GradeAverageMode.ALL_YEAR) 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) + } returns resourceFlow { Triple(firstGrades, firstSummaries, emptyList()) } + coEvery { + gradeRepository.getGrades( + student, + semesters[2], + true + ) + } returns resourceFlow { + Triple( + secondGrades, listOf( + getSummary(22, "Matematyka", 1.1), + getSummary(22, "Fizyka", 7.26) + ), emptyList() ) } @@ -646,9 +944,9 @@ class GradeAverageProviderTest { @Test fun `calc all year average`() { - every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.ALL_YEAR - every { preferencesRepository.gradeAverageForceCalc } returns false - every { preferencesRepository.isOptionalArithmeticAverage } returns false + every { preferencesRepository.gradeAverageModeFlow } returns flowOf(GradeAverageMode.ALL_YEAR) + every { preferencesRepository.gradeAverageForceCalcFlow } returns flowOf(false) + every { preferencesRepository.isOptionalArithmeticAverageFlow } returns flowOf(false) coEvery { gradeRepository.getGrades( student, @@ -656,9 +954,11 @@ class GradeAverageProviderTest { true ) } returns resourceFlow { - firstGrades to listOf( - getSummary(22, "Matematyka", .0), - getSummary(22, "Fizyka", .0) + Triple( + firstGrades, listOf( + getSummary(22, "Matematyka", .0), + getSummary(22, "Fizyka", .0) + ), emptyList() ) } coEvery { @@ -668,9 +968,11 @@ class GradeAverageProviderTest { true ) } returns resourceFlow { - secondGrades to listOf( - getSummary(22, "Matematyka", .0), - getSummary(22, "Fizyka", .0) + Triple( + secondGrades, listOf( + getSummary(22, "Matematyka", .0), + getSummary(22, "Fizyka", .0) + ), emptyList() ) } @@ -689,36 +991,46 @@ class GradeAverageProviderTest { @Test fun `force calc full year average when current is second with load from cache sequence`() { - every { preferencesRepository.gradeAverageForceCalc } returns true - every { preferencesRepository.isOptionalArithmeticAverage } returns false - every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.ALL_YEAR + every { preferencesRepository.gradeAverageForceCalcFlow } returns flowOf(true) + every { preferencesRepository.isOptionalArithmeticAverageFlow } returns flowOf(false) + every { preferencesRepository.gradeAverageModeFlow } returns flowOf(GradeAverageMode.ALL_YEAR) coEvery { semesterRepository.getSemesters(student) } returns semesters coEvery { gradeRepository.getGrades(student, semesters[1], true) } returns flow { emit(Resource.Loading()) - emit(Resource.Intermediate(firstGrades to firstSummaries)) - emit(Resource.Success(firstGrades to firstSummaries)) + emit(Resource.Intermediate(Triple(firstGrades, firstSummaries, emptyList()))) + emit(Resource.Success(Triple(firstGrades, firstSummaries, emptyList()))) } coEvery { gradeRepository.getGrades(student, semesters[2], true) } returns flow { emit(Resource.Loading()) emit( Resource.Intermediate( - secondGrades to listOf( - getSummary(22, "Matematyka", 1.1), - getSummary(22, "Fizyka", 7.26) + Triple( + secondGrades, listOf( + getSummary(22, "Matematyka", 1.1), + getSummary(22, "Fizyka", 7.26) + ), emptyList() ) ) ) emit( Resource.Success( - secondGrades to listOf( - getSummary(22, "Matematyka", 1.1), - getSummary(22, "Fizyka", 7.26) + Triple( + secondGrades, listOf( + getSummary(22, "Matematyka", 1.1), + getSummary(22, "Fizyka", 7.26) + ), emptyList() ) ) ) } - val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId, true).toList() } + val items = runBlocking { + gradeAverageProvider.getGradesDetailsWithAverage( + student, + semesters[2].semesterId, + true + ).toList() + } with(items[0]) { assertEquals(Status.LOADING, status) @@ -734,15 +1046,23 @@ class GradeAverageProviderTest { } 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 + 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 fun `force calc both semesters average when no summaries`() { - every { preferencesRepository.gradeAverageForceCalc } returns true - every { preferencesRepository.isOptionalArithmeticAverage } returns false - every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.BOTH_SEMESTERS + every { preferencesRepository.gradeAverageForceCalcFlow } returns flowOf(true) + every { preferencesRepository.isOptionalArithmeticAverageFlow } returns flowOf(false) + every { preferencesRepository.gradeAverageModeFlow } returns flowOf(GradeAverageMode.BOTH_SEMESTERS) coEvery { gradeRepository.getGrades( @@ -750,14 +1070,14 @@ class GradeAverageProviderTest { semesters[1], true ) - } returns resourceFlow { firstGrades to emptyList() } + } returns resourceFlow { Triple(firstGrades, emptyList(), emptyList()) } coEvery { gradeRepository.getGrades( student, semesters[2], true ) - } returns resourceFlow { secondGrades to emptyList() } + } returns resourceFlow { Triple(secondGrades, emptyList(), emptyList()) } val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage( @@ -773,14 +1093,18 @@ class GradeAverageProviderTest { 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.25, + items.single { it.subject == "Fizyka" }.average, + .0 + ) // (from details): 3,5 + 3,0 → 3,25 } @Test fun `force calc full year average when no summaries`() { - every { preferencesRepository.gradeAverageForceCalc } returns true - every { preferencesRepository.isOptionalArithmeticAverage } returns false - every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.ALL_YEAR + every { preferencesRepository.gradeAverageForceCalcFlow } returns flowOf(true) + every { preferencesRepository.isOptionalArithmeticAverageFlow } returns flowOf(false) + every { preferencesRepository.gradeAverageModeFlow } returns flowOf(GradeAverageMode.ALL_YEAR) coEvery { gradeRepository.getGrades( @@ -788,14 +1112,14 @@ class GradeAverageProviderTest { semesters[1], true ) - } returns resourceFlow { firstGrades to emptyList() } + } returns resourceFlow { Triple(firstGrades, emptyList(), emptyList()) } coEvery { gradeRepository.getGrades( student, semesters[2], true ) - } returns resourceFlow { secondGrades to emptyList() } + } returns resourceFlow { Triple(secondGrades, emptyList(), emptyList()) } val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage( @@ -820,33 +1144,9 @@ class GradeAverageProviderTest { @Test fun `calc both semesters average when missing summaries in both semesters`() { - every { preferencesRepository.gradeAverageForceCalc } returns false - every { preferencesRepository.isOptionalArithmeticAverage } returns false - every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.BOTH_SEMESTERS - - 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 resourceFlow { - secondGrades to listOf( - getSummary(23, "Matematyka", 3.0) - ) - } - - val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId, true).getResult() } - - assertEquals(2, items.size) - assertEquals(3.5, items.single { it.subject == "Matematyka" }.average, .0) // (from summaries ↑): 4,0 + 3,0 → 3,5 - assertEquals(3.25, items.single { it.subject == "Fizyka" }.average, .0) // (from details): 3,5 + 3,0 → 3,25 - } - - @Test - fun `calc both semesters average when missing summary in second semester`() { - every { preferencesRepository.gradeAverageForceCalc } returns false - every { preferencesRepository.isOptionalArithmeticAverage } returns false - every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.BOTH_SEMESTERS + every { preferencesRepository.gradeAverageForceCalcFlow } returns flowOf(false) + every { preferencesRepository.isOptionalArithmeticAverageFlow } returns flowOf(false) + every { preferencesRepository.gradeAverageModeFlow } returns flowOf(GradeAverageMode.BOTH_SEMESTERS) coEvery { gradeRepository.getGrades( @@ -854,14 +1154,74 @@ class GradeAverageProviderTest { semesters[1], true ) - } returns resourceFlow { firstGrades to firstSummaries } + } returns resourceFlow { + Triple( + firstGrades, listOf( + getSummary(22, "Matematyka", 4.0) + ), emptyList() + ) + } coEvery { gradeRepository.getGrades( student, semesters[2], true ) - } returns resourceFlow { secondGrades to secondSummaries.dropLast(1) } + } returns resourceFlow { + Triple( + secondGrades, listOf( + getSummary(23, "Matematyka", 3.0) + ), emptyList() + ) + } + + val items = runBlocking { + gradeAverageProvider.getGradesDetailsWithAverage( + student, + semesters[2].semesterId, + true + ).getResult() + } + + assertEquals(2, items.size) + assertEquals( + 3.5, + items.single { it.subject == "Matematyka" }.average, + .0 + ) // (from summaries ↑): 4,0 + 3,0 → 3,5 + assertEquals( + 3.25, + items.single { it.subject == "Fizyka" }.average, + .0 + ) // (from details): 3,5 + 3,0 → 3,25 + } + + @Test + fun `calc both semesters average when missing summary in second semester`() { + every { preferencesRepository.gradeAverageForceCalcFlow } returns flowOf(false) + every { preferencesRepository.isOptionalArithmeticAverageFlow } returns flowOf(false) + every { preferencesRepository.gradeAverageModeFlow } returns flowOf(GradeAverageMode.BOTH_SEMESTERS) + + coEvery { + gradeRepository.getGrades( + student, + semesters[1], + true + ) + } returns resourceFlow { Triple(firstGrades, firstSummaries, emptyList()) } + coEvery { + gradeRepository.getGrades( + student, + semesters[2], + true + ) + } returns resourceFlow { + Triple( + secondGrades, + secondSummaries.dropLast(1), + emptyList() + ) + } val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage( @@ -886,9 +1246,9 @@ class GradeAverageProviderTest { @Test fun `calc both semesters average when missing summary in first semester`() { - every { preferencesRepository.gradeAverageForceCalc } returns false - every { preferencesRepository.isOptionalArithmeticAverage } returns false - every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.BOTH_SEMESTERS + every { preferencesRepository.gradeAverageForceCalcFlow } returns flowOf(false) + every { preferencesRepository.isOptionalArithmeticAverageFlow } returns flowOf(false) + every { preferencesRepository.gradeAverageModeFlow } returns flowOf(GradeAverageMode.BOTH_SEMESTERS) coEvery { gradeRepository.getGrades( @@ -896,14 +1256,20 @@ class GradeAverageProviderTest { semesters[1], true ) - } returns resourceFlow { firstGrades to firstSummaries.dropLast(1) } + } returns resourceFlow { + Triple( + firstGrades, + firstSummaries.dropLast(1), + emptyList() + ) + } coEvery { gradeRepository.getGrades( student, semesters[2], true ) - } returns resourceFlow { secondGrades to secondSummaries } + } returns resourceFlow { Triple(secondGrades, secondSummaries, emptyList()) } val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage( @@ -928,9 +1294,9 @@ class GradeAverageProviderTest { @Test fun `force calc full year average when missing summary in first semester`() { - every { preferencesRepository.gradeAverageForceCalc } returns true - every { preferencesRepository.isOptionalArithmeticAverage } returns false - every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.ALL_YEAR + every { preferencesRepository.gradeAverageForceCalcFlow } returns flowOf(true) + every { preferencesRepository.isOptionalArithmeticAverageFlow } returns flowOf(false) + every { preferencesRepository.gradeAverageModeFlow } returns flowOf(GradeAverageMode.ALL_YEAR) coEvery { gradeRepository.getGrades( @@ -938,14 +1304,20 @@ class GradeAverageProviderTest { semesters[1], true ) - } returns resourceFlow { firstGrades to firstSummaries.dropLast(1) } + } returns resourceFlow { + Triple( + firstGrades, + firstSummaries.dropLast(1), + emptyList() + ) + } coEvery { gradeRepository.getGrades( student, semesters[2], true ) - } returns resourceFlow { secondGrades to secondSummaries } + } returns resourceFlow { Triple(secondGrades, secondSummaries, emptyList()) } val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage( @@ -970,133 +1342,219 @@ class GradeAverageProviderTest { @Test fun `force calc both semesters average with different average from all grades and from two semesters`() { - every { preferencesRepository.gradeAverageForceCalc } returns true - every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.BOTH_SEMESTERS - every { preferencesRepository.isOptionalArithmeticAverage } returns false + every { preferencesRepository.gradeAverageForceCalcFlow } returns flowOf(true) + every { preferencesRepository.gradeAverageModeFlow } returns flowOf(GradeAverageMode.BOTH_SEMESTERS) + every { preferencesRepository.isOptionalArithmeticAverageFlow } returns flowOf(false) - coEvery { gradeRepository.getGrades(student, semesters[1], true) } returns resourceFlow { - listOf( - getGrade(22, "Fizyka", 5.0, weight = 2.0), - getGrade(22, "Fizyka", 6.0, weight = 2.0), - getGrade(22, "Fizyka", 5.0, weight = 4.0), - getGrade(22, "Fizyka", 6.0, weight = 2.0), - getGrade(22, "Fizyka", 6.0, weight = 2.0), - getGrade(22, "Fizyka", 6.0, weight = 4.0), - getGrade(22, "Fizyka", 6.0, weight = 4.0), - getGrade(22, "Fizyka", 6.0, weight = 2.0) - ) to listOf(getSummary(semesterId = 22, subject = "Fizyka", average = .0)) + coEvery { + gradeRepository.getGrades( + student, + semesters[1], + true + ) + } returns resourceFlow { + Triple( + listOf( + getGrade(22, "Fizyka", 5.0, weight = 2.0), + getGrade(22, "Fizyka", 6.0, weight = 2.0), + getGrade(22, "Fizyka", 5.0, weight = 4.0), + getGrade(22, "Fizyka", 6.0, weight = 2.0), + getGrade(22, "Fizyka", 6.0, weight = 2.0), + getGrade(22, "Fizyka", 6.0, weight = 4.0), + getGrade(22, "Fizyka", 6.0, weight = 4.0), + getGrade(22, "Fizyka", 6.0, weight = 2.0) + ), + listOf(getSummary(semesterId = 22, subject = "Fizyka", average = .0)), + emptyList() + ) } - coEvery { gradeRepository.getGrades(student, semesters[2], true) } returns resourceFlow { - listOf( - getGrade(23, "Fizyka", 5.0, weight = 1.0), - getGrade(23, "Fizyka", 5.0, weight = 2.0), - getGrade(23, "Fizyka", 4.0, modifier = 0.3, weight = 2.0) - ) to listOf(getSummary(semesterId = 23, subject = "Fizyka", average = .0)) + coEvery { + gradeRepository.getGrades( + student, + semesters[2], + true + ) + } returns resourceFlow { + Triple( + listOf( + getGrade(23, "Fizyka", 5.0, weight = 1.0), + getGrade(23, "Fizyka", 5.0, weight = 2.0), + getGrade(23, "Fizyka", 4.0, modifier = 0.3, weight = 2.0) + ), + listOf(getSummary(semesterId = 23, subject = "Fizyka", average = .0)), + emptyList() + ) } - val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId, true).getResult() } + val items = runBlocking { + gradeAverageProvider.getGradesDetailsWithAverage( + student, + semesters[2].semesterId, + true + ).getResult() + } - assertEquals(5.2296, items.single { it.subject == "Fizyka" }.average, .0001) // (from details): 5.72727272 + 4,732 → 5.229636363636364 + assertEquals( + 5.2296, + items.single { it.subject == "Fizyka" }.average, + .0001 + ) // (from details): 5.72727272 + 4,732 → 5.229636363636364 } @Test fun `force calc full year average with different average from all grades and from two semesters`() { - every { preferencesRepository.gradeAverageForceCalc } returns true - every { preferencesRepository.isOptionalArithmeticAverage } returns false - every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.ALL_YEAR + every { preferencesRepository.gradeAverageForceCalcFlow } returns flowOf(true) + every { preferencesRepository.isOptionalArithmeticAverageFlow } returns flowOf(false) + every { preferencesRepository.gradeAverageModeFlow } returns flowOf(GradeAverageMode.ALL_YEAR) coEvery { gradeRepository.getGrades(student, semesters[1], true) } returns resourceFlow { - listOf( - getGrade(22, "Fizyka", 5.0, weight = 2.0), - getGrade(22, "Fizyka", 6.0, weight = 2.0), - getGrade(22, "Fizyka", 5.0, weight = 4.0), - getGrade(22, "Fizyka", 6.0, weight = 2.0), - getGrade(22, "Fizyka", 6.0, weight = 2.0), - getGrade(22, "Fizyka", 6.0, weight = 4.0), - getGrade(22, "Fizyka", 6.0, weight = 4.0), - getGrade(22, "Fizyka", 6.0, weight = 2.0) - ) to listOf(getSummary(semesterId = 22, subject = "Fizyka", average = .0)) + Triple( + listOf( + getGrade(22, "Fizyka", 5.0, weight = 2.0), + getGrade(22, "Fizyka", 6.0, weight = 2.0), + getGrade(22, "Fizyka", 5.0, weight = 4.0), + getGrade(22, "Fizyka", 6.0, weight = 2.0), + getGrade(22, "Fizyka", 6.0, weight = 2.0), + getGrade(22, "Fizyka", 6.0, weight = 4.0), + getGrade(22, "Fizyka", 6.0, weight = 4.0), + getGrade(22, "Fizyka", 6.0, weight = 2.0) + ), + listOf(getSummary(semesterId = 22, subject = "Fizyka", average = .0)), + emptyList() + ) } coEvery { gradeRepository.getGrades(student, semesters[2], true) } returns resourceFlow { - listOf( - getGrade(23, "Fizyka", 5.0, weight = 1.0), - getGrade(23, "Fizyka", 5.0, weight = 2.0), - getGrade(23, "Fizyka", 4.0, modifier = 0.3, weight = 2.0) - ) to listOf(getSummary(semesterId = 23, subject = "Fizyka", average = .0)) + Triple( + listOf( + getGrade(23, "Fizyka", 5.0, weight = 1.0), + getGrade(23, "Fizyka", 5.0, weight = 2.0), + getGrade(23, "Fizyka", 4.0, modifier = 0.3, weight = 2.0) + ), + listOf(getSummary(semesterId = 23, subject = "Fizyka", average = .0)), + emptyList() + ) } - val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId, true).getResult() } + val items = runBlocking { + gradeAverageProvider.getGradesDetailsWithAverage( + student, + semesters[2].semesterId, + true + ).getResult() + } - assertEquals(5.5429, items.single { it.subject == "Fizyka" }.average, .0001) // (from details): 5.72727272 + 4,732 → .average() + assertEquals( + 5.5429, + items.single { it.subject == "Fizyka" }.average, + .0001 + ) // (from details): 5.72727272 + 4,732 → .average() } @Test fun `force calc both semesters average with different average from all grades and from two semesters with custom modifiers`() { val student = student.copy(loginMode = Sdk.Mode.SCRAPPER.name) - every { preferencesRepository.gradeAverageForceCalc } returns true - every { preferencesRepository.isOptionalArithmeticAverage } returns false - every { preferencesRepository.gradeMinusModifier } returns .33 - every { preferencesRepository.gradePlusModifier } returns .5 + every { preferencesRepository.gradeAverageForceCalcFlow } returns flowOf(true) + every { preferencesRepository.isOptionalArithmeticAverageFlow } returns flowOf(false) + every { preferencesRepository.gradeMinusModifierFlow } returns flowOf(.33) + every { preferencesRepository.gradePlusModifierFlow } returns flowOf(.5) - every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.BOTH_SEMESTERS + every { preferencesRepository.gradeAverageModeFlow } returns flowOf(GradeAverageMode.BOTH_SEMESTERS) coEvery { semesterRepository.getSemesters(student) } returns semesters coEvery { gradeRepository.getGrades(student, semesters[1], true) } returns resourceFlow { - listOf( - getGrade(22, "Fizyka", 5.0, weight = 2.0), - getGrade(22, "Fizyka", 6.0, weight = 2.0), - getGrade(22, "Fizyka", 5.0, weight = 4.0), - getGrade(22, "Fizyka", 6.0, weight = 2.0), - getGrade(22, "Fizyka", 6.0, weight = 2.0), - getGrade(22, "Fizyka", 6.0, weight = 4.0), - getGrade(22, "Fizyka", 6.0, weight = 4.0), - getGrade(22, "Fizyka", 6.0, weight = 2.0) - ) to listOf(getSummary(semesterId = 22, subject = "Fizyka", average = .0)) + Triple( + listOf( + getGrade(22, "Fizyka", 5.0, weight = 2.0), + getGrade(22, "Fizyka", 6.0, weight = 2.0), + getGrade(22, "Fizyka", 5.0, weight = 4.0), + getGrade(22, "Fizyka", 6.0, weight = 2.0), + getGrade(22, "Fizyka", 6.0, weight = 2.0), + getGrade(22, "Fizyka", 6.0, weight = 4.0), + getGrade(22, "Fizyka", 6.0, weight = 4.0), + getGrade(22, "Fizyka", 6.0, weight = 2.0) + ), + listOf(getSummary(semesterId = 22, subject = "Fizyka", average = .0)), + emptyList() + ) } coEvery { gradeRepository.getGrades(student, semesters[2], true) } returns resourceFlow { - listOf( - getGrade(23, "Fizyka", 5.0, weight = 1.0), - getGrade(23, "Fizyka", 5.0, weight = 2.0), - getGrade(23, "Fizyka", 4.0, modifier = 0.33, weight = 2.0) - ) to listOf(getSummary(semesterId = 23, subject = "Fizyka", average = .0)) + Triple( + listOf( + getGrade(23, "Fizyka", 5.0, weight = 1.0), + getGrade(23, "Fizyka", 5.0, weight = 2.0), + getGrade(23, "Fizyka", 4.0, modifier = 0.33, weight = 2.0) + ), + listOf(getSummary(semesterId = 23, subject = "Fizyka", average = .0)), + emptyList() + ) } - val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId, true).getResult() } + val items = runBlocking { + gradeAverageProvider.getGradesDetailsWithAverage( + student, + semesters[2].semesterId, + true + ).getResult() + } - assertEquals(5.2636, items.single { it.subject == "Fizyka" }.average, .0001) // (from details): 5.72727272 + 4,8 → 5.26363636 + assertEquals( + 5.2636, + items.single { it.subject == "Fizyka" }.average, + .0001 + ) // (from details): 5.72727272 + 4,8 → 5.26363636 } @Test fun `force calc full year average with different average from all grades and from two semesters with custom modifiers`() { val student = student.copy(loginMode = Sdk.Mode.SCRAPPER.name) - every { preferencesRepository.gradeAverageForceCalc } returns true - every { preferencesRepository.isOptionalArithmeticAverage } returns false - every { preferencesRepository.gradeMinusModifier } returns .33 - every { preferencesRepository.gradePlusModifier } returns .5 + every { preferencesRepository.gradeAverageForceCalcFlow } returns flowOf(true) + every { preferencesRepository.isOptionalArithmeticAverageFlow } returns flowOf(false) + every { preferencesRepository.gradeMinusModifierFlow } returns flowOf(.33) + every { preferencesRepository.gradePlusModifierFlow } returns flowOf(.5) - every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.ALL_YEAR + every { preferencesRepository.gradeAverageModeFlow } returns flowOf(GradeAverageMode.ALL_YEAR) coEvery { semesterRepository.getSemesters(student) } returns semesters - coEvery { gradeRepository.getGrades(student, semesters[1], true) } returns resourceFlow { - listOf( - getGrade(22, "Fizyka", 5.0, weight = 2.0), - getGrade(22, "Fizyka", 6.0, weight = 2.0), - getGrade(22, "Fizyka", 5.0, weight = 4.0), - getGrade(22, "Fizyka", 6.0, weight = 2.0), - getGrade(22, "Fizyka", 6.0, weight = 2.0), - getGrade(22, "Fizyka", 6.0, weight = 4.0), - getGrade(22, "Fizyka", 6.0, weight = 4.0), - getGrade(22, "Fizyka", 6.0, weight = 2.0) - ) to listOf(getSummary(semesterId = 22, subject = "Fizyka", average = .0)) + coEvery { + gradeRepository.getGrades( + student, + semesters[1], + true + ) + } returns resourceFlow { + Triple( + listOf( + getGrade(22, "Fizyka", 5.0, weight = 2.0), + getGrade(22, "Fizyka", 6.0, weight = 2.0), + getGrade(22, "Fizyka", 5.0, weight = 4.0), + getGrade(22, "Fizyka", 6.0, weight = 2.0), + getGrade(22, "Fizyka", 6.0, weight = 2.0), + getGrade(22, "Fizyka", 6.0, weight = 4.0), + getGrade(22, "Fizyka", 6.0, weight = 4.0), + getGrade(22, "Fizyka", 6.0, weight = 2.0) + ), + listOf(getSummary(semesterId = 22, subject = "Fizyka", average = .0)), + emptyList() + ) } - coEvery { gradeRepository.getGrades(student, semesters[2], true) } returns resourceFlow { - listOf( - getGrade(23, "Fizyka", 5.0, weight = 1.0), - getGrade(23, "Fizyka", 5.0, weight = 2.0), - getGrade(23, "Fizyka", 4.0, modifier = 0.33, weight = 2.0) - ) to listOf(getSummary(semesterId = 23, subject = "Fizyka", average = .0)) + coEvery { + gradeRepository.getGrades( + student, + semesters[2], + true + ) + } returns resourceFlow { + Triple( + listOf( + getGrade(23, "Fizyka", 5.0, weight = 1.0), + getGrade(23, "Fizyka", 5.0, weight = 2.0), + getGrade(23, "Fizyka", 4.0, modifier = 0.33, weight = 2.0) + ), + listOf(getSummary(semesterId = 23, subject = "Fizyka", average = .0)), + emptyList() + ) } val items = runBlocking { @@ -1116,20 +1574,20 @@ class GradeAverageProviderTest { @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 + every { preferencesRepository.gradeAverageForceCalcFlow } returns flowOf(false) + every { preferencesRepository.gradeAverageModeFlow } returns flowOf(GradeAverageMode.BOTH_SEMESTERS) + every { preferencesRepository.isOptionalArithmeticAverageFlow } returns flowOf(false) coEvery { gradeRepository.getGrades(student, semesters[1], true) } returns - resourceFlow { firstGrades to firstSummaries } + resourceFlow { Triple(firstGrades, firstSummaries, emptyList()) } coEvery { gradeRepository.getGrades(student, semesters[2], true) } returns - resourceFlow { listOf() to firstSummaries } + resourceFlow { Triple(listOf(), firstSummaries, emptyList()) } val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage( - student, - semesters[2].semesterId, - true + student = student, + semesterId = semesters[2].semesterId, + forceRefresh = true, ).getResult() } 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 9bcfb8b6c..cf43df63b 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 @@ -1,18 +1,26 @@ package io.github.wulkanowy.ui.modules.login.form 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.pojos.RegisterUser +import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.StudentRepository +import io.github.wulkanowy.domain.adminmessage.GetAppropriateAdminMessageUseCase +import io.github.wulkanowy.sdk.Sdk +import io.github.wulkanowy.sdk.scrapper.Scrapper import io.github.wulkanowy.ui.modules.login.LoginErrorHandler import io.github.wulkanowy.utils.AnalyticsHelper -import io.mockk.* +import io.github.wulkanowy.utils.AppInfo +import io.mockk.MockKAnnotations +import io.mockk.Runs +import io.mockk.coEvery +import io.mockk.every 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.Instant class LoginFormPresenterTest { @@ -31,8 +39,27 @@ class LoginFormPresenterTest { @MockK(relaxed = true) lateinit var analytics: AnalyticsHelper + @MockK + lateinit var appInfo: AppInfo + + @MockK + lateinit var getAppropriateAdminMessageUseCase: GetAppropriateAdminMessageUseCase + + @MockK + lateinit var preferencesRepository: PreferencesRepository + private lateinit var presenter: LoginFormPresenter + private val registerUser = RegisterUser( + email = "", + password = "", + login = "", + scrapperBaseUrl = "", + loginMode = Sdk.Mode.HEBE, + loginType = Scrapper.LoginType.AUTO, + symbols = listOf(), + ) + @Before fun setUp() { MockKAnnotations.init(this) @@ -46,8 +73,16 @@ class LoginFormPresenterTest { every { loginFormView.setErrorPassInvalid(any()) } just Runs every { loginFormView.setErrorPassRequired(any()) } just Runs every { loginFormView.setErrorUsernameRequired() } just Runs + every { appInfo.isDebug } returns false - presenter = LoginFormPresenter(repository, errorHandler, analytics) + presenter = LoginFormPresenter( + studentRepository = repository, + loginErrorHandler = errorHandler, + appInfo = appInfo, + analytics = analytics, + getAppropriateAdminMessageUseCase = getAppropriateAdminMessageUseCase, + preferencesRepository = preferencesRepository, + ) presenter.onAttachView(loginFormView) } @@ -104,32 +139,9 @@ class LoginFormPresenterTest { @Test fun loginTest() { - val studentTest = Student( - email = "test@", - password = "123", - scrapperBaseUrl = "https://fakelog.cf/?email", - loginType = "AUTO", - studentName = "", - schoolSymbol = "", - schoolName = "", - studentId = 0, - classId = 1, - isCurrent = false, - symbol = "", - registrationDate = Instant.now(), - className = "", - mobileBaseUrl = "", - privateKey = "", - certificateKey = "", - loginMode = "", - userLoginId = 0, - schoolShortName = "", - isParent = false, - userName = "" - ) - coEvery { repository.getStudentsScrapper(any(), any(), any(), any()) } returns listOf( - StudentWithSemesters(studentTest, emptyList()) - ) + coEvery { + repository.getUserSubjectsFromScrapper(any(), any(), any(), any(), any()) + } returns registerUser every { loginFormView.formUsernameValue } returns "@" every { loginFormView.formPassValue } returns "123456" @@ -146,7 +158,9 @@ class LoginFormPresenterTest { @Test fun loginEmptyTest() { - coEvery { repository.getStudentsScrapper(any(), any(), any(), any()) } returns listOf() + coEvery { + repository.getUserSubjectsFromScrapper(any(), any(), any(), any(), any()) + } returns registerUser every { loginFormView.formUsernameValue } returns "@" every { loginFormView.formPassValue } returns "123456" every { loginFormView.formHostValue } returns "https://fakelog.cf/?email" @@ -162,7 +176,9 @@ class LoginFormPresenterTest { @Test fun loginEmptyTwiceTest() { - coEvery { repository.getStudentsScrapper(any(), any(), any(), any()) } returns listOf() + coEvery { + repository.getUserSubjectsFromScrapper(any(), any(), any(), any(), any()) + } returns registerUser every { loginFormView.formUsernameValue } returns "@" every { loginFormView.formPassValue } returns "123456" every { loginFormView.formHostValue } returns "https://fakelog.cf/?email" @@ -180,7 +196,9 @@ class LoginFormPresenterTest { @Test fun loginErrorTest() { val testException = IOException("test") - coEvery { repository.getStudentsScrapper(any(), any(), any(), any()) } throws testException + coEvery { + repository.getUserSubjectsFromScrapper(any(), any(), any(), any(), any()) + } throws testException every { loginFormView.formUsernameValue } returns "@" every { loginFormView.formPassValue } returns "123456" every { loginFormView.formHostValue } returns "https://fakelog.cf/?email" 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 e52ec3ae2..34965f00d 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 @@ -1,18 +1,31 @@ package io.github.wulkanowy.ui.modules.login.studentselect 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.pojos.RegisterStudent +import io.github.wulkanowy.data.pojos.RegisterSymbol +import io.github.wulkanowy.data.pojos.RegisterUnit +import io.github.wulkanowy.data.pojos.RegisterUser +import io.github.wulkanowy.data.repositories.SchoolsRepository import io.github.wulkanowy.data.repositories.StudentRepository +import io.github.wulkanowy.sdk.Sdk +import io.github.wulkanowy.sdk.scrapper.Scrapper import io.github.wulkanowy.services.sync.SyncManager +import io.github.wulkanowy.ui.modules.login.LoginData import io.github.wulkanowy.ui.modules.login.LoginErrorHandler import io.github.wulkanowy.utils.AnalyticsHelper -import io.mockk.* +import io.github.wulkanowy.utils.AppInfo +import io.mockk.MockKAnnotations +import io.mockk.Runs +import io.mockk.clearMocks +import io.mockk.coEvery +import io.mockk.every import io.mockk.impl.annotations.MockK +import io.mockk.just +import io.mockk.slot +import io.mockk.verify import org.junit.Before import org.junit.Rule import org.junit.Test -import java.time.Instant class LoginStudentSelectPresenterTest { @@ -22,45 +35,75 @@ class LoginStudentSelectPresenterTest { @MockK(relaxed = true) lateinit var errorHandler: LoginErrorHandler - @MockK(relaxed = true) + @MockK lateinit var loginStudentSelectView: LoginStudentSelectView @MockK lateinit var studentRepository: StudentRepository + @MockK + lateinit var schoolsRepository: SchoolsRepository + @MockK(relaxed = true) lateinit var analytics: AnalyticsHelper @MockK(relaxed = true) lateinit var syncManager: SyncManager + private val appInfo = AppInfo() + private lateinit var presenter: LoginStudentSelectPresenter - private val testStudent by lazy { - Student( - email = "test", - password = "test123", - scrapperBaseUrl = "https://fakelog.cf", - loginType = "AUTO", - symbol = "", - isCurrent = false, - studentId = 0, - schoolName = "", - schoolSymbol = "", - classId = 1, - studentName = "", - registrationDate = Instant.now(), - className = "", - loginMode = "", - certificateKey = "", - privateKey = "", - mobileBaseUrl = "", - schoolShortName = "", - userLoginId = 1, - isParent = false, - userName = "" - ) - } + private val loginData = LoginData( + login = "", + password = "", + baseUrl = "", + defaultSymbol = "warszawa", + domainSuffix = "", + ) + + private val subject = RegisterStudent( + studentId = 0, + studentName = "", + studentSecondName = "", + studentSurname = "", + className = "", + classId = 0, + isParent = false, + semesters = listOf(), + ) + + private val school = RegisterUnit( + userLoginId = 0, + schoolId = "", + schoolName = "", + schoolShortName = "", + parentIds = listOf(), + studentIds = listOf(), + employeeIds = listOf(), + error = null, + students = listOf(subject) + ) + + private val symbol = RegisterSymbol( + symbol = "", + error = null, + userName = "", + keyId = null, + privatePem = null, + hebeBaseUrl = null, + schools = listOf(school), + ) + + private val registerUser = RegisterUser( + email = "", + password = "", + login = "", + scrapperBaseUrl = "", + loginMode = Sdk.Mode.SCRAPPER, + loginType = Scrapper.LoginType.AUTO, + symbols = listOf(symbol), + ) private val testException by lazy { RuntimeException("Problem") } @@ -69,46 +112,64 @@ class LoginStudentSelectPresenterTest { MockKAnnotations.init(this) clearMocks(studentRepository, loginStudentSelectView) + + coEvery { studentRepository.getSavedStudents(false) } returns emptyList() + coEvery { schoolsRepository.logSchoolLogin(any(), any()) } just Runs + every { loginStudentSelectView.initView() } just Runs - every { loginStudentSelectView.showContact(any()) } just Runs + every { loginStudentSelectView.symbols } returns emptyMap() + every { loginStudentSelectView.enableSignIn(any()) } just Runs every { loginStudentSelectView.showProgress(any()) } just Runs every { loginStudentSelectView.showContent(any()) } just Runs - presenter = LoginStudentSelectPresenter(studentRepository, errorHandler, syncManager, analytics) - presenter.onAttachView(loginStudentSelectView, emptyList()) + presenter = LoginStudentSelectPresenter( + studentRepository = studentRepository, + schoolsRepository = schoolsRepository, + loginErrorHandler = errorHandler, + syncManager = syncManager, + analytics = analytics, + appInfo = appInfo, + ) } @Test fun initViewTest() { + presenter.onAttachView(loginStudentSelectView, loginData, registerUser) verify { loginStudentSelectView.initView() } } @Test fun onSelectedStudentTest() { - coEvery { - studentRepository.saveStudents(listOf(StudentWithSemesters(testStudent, emptyList()))) - } just Runs + val itemsSlot = slot>() + every { loginStudentSelectView.updateData(capture(itemsSlot)) } just Runs + presenter.onAttachView(loginStudentSelectView, loginData, registerUser) - every { loginStudentSelectView.openMainView() } just Runs + coEvery { studentRepository.saveStudents(any()) } just Runs - presenter.onItemSelected(StudentWithSemesters(testStudent, emptyList()), false) + every { loginStudentSelectView.navigateToNext() } just Runs + + itemsSlot.captured.filterIsInstance().first().let { + it.onClick(it) + } presenter.onSignIn() verify { loginStudentSelectView.showContent(false) } verify { loginStudentSelectView.showProgress(true) } - verify { loginStudentSelectView.openMainView() } + verify { loginStudentSelectView.navigateToNext() } } @Test fun onSelectedStudentErrorTest() { - coEvery { - studentRepository.saveStudents(listOf(StudentWithSemesters(testStudent, emptyList()))) - } throws testException + val itemsSlot = slot>() + every { loginStudentSelectView.updateData(capture(itemsSlot)) } just Runs + presenter.onAttachView(loginStudentSelectView, loginData, registerUser) - coEvery { studentRepository.logoutStudent(testStudent) } just Runs + coEvery { studentRepository.saveStudents(any()) } throws testException - presenter.onItemSelected(StudentWithSemesters(testStudent, emptyList()), false) + itemsSlot.captured.filterIsInstance().first().let { + it.onClick(it) + } presenter.onSignIn() verify { loginStudentSelectView.showContent(false) } 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 6cfab1995..460c8385f 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 @@ -46,7 +46,7 @@ class MainPresenterTest { MockKAnnotations.init(this) clearMocks(mainView) - every { mainView.initView(any(), any()) } just Runs + every { mainView.initView(any(), any(), any()) } just Runs presenter = MainPresenter( errorHandler = errorHandler, studentRepository = studentRepository, diff --git a/app/src/test/java/io/github/wulkanowy/utils/SemesterExtensionKtTest.kt b/app/src/test/java/io/github/wulkanowy/utils/SemesterExtensionKtTest.kt index b7d3ecc94..e8ba8a876 100644 --- a/app/src/test/java/io/github/wulkanowy/utils/SemesterExtensionKtTest.kt +++ b/app/src/test/java/io/github/wulkanowy/utils/SemesterExtensionKtTest.kt @@ -8,6 +8,40 @@ import kotlin.test.assertEquals class SemesterExtensionKtTest { + @Test + fun `check is first semester is current`() { + val first = getSemesterEntity( + semesterName = 1, + start = LocalDate.of(2023, 9, 1), + end = LocalDate.of(2024, 1, 31), + ) + + // first boundary - school-year start + assertEquals(false, first.isCurrent(LocalDate.of(2023, 8, 28))) + assertEquals(true, first.isCurrent(LocalDate.of(2023, 8, 29))) + + // second boundary + assertEquals(true, first.isCurrent(LocalDate.of(2024, 1, 31))) + assertEquals(false, first.isCurrent(LocalDate.of(2024, 2, 1))) + } + + @Test + fun `check is second semester is current`() { + val second = getSemesterEntity( + semesterName = 2, + start = LocalDate.of(2024, 2, 1), + end = LocalDate.of(2024, 9, 1), + ) + + // first boundary + assertEquals(false, second.isCurrent(LocalDate.of(2024, 1, 31))) + assertEquals(true, second.isCurrent(LocalDate.of(2024, 2, 1))) + + // second boundary - school-year end + assertEquals(true, second.isCurrent(LocalDate.of(2024, 8, 29))) + assertEquals(false, second.isCurrent(LocalDate.of(2024, 8, 30))) + } + @Test(expected = IllegalArgumentException::class) fun `get current semester when current is doubled`() { val semesters = listOf( diff --git a/build.gradle b/build.gradle index 98c9dfb84..f7f3d209e 100644 --- a/build.gradle +++ b/build.gradle @@ -1,8 +1,8 @@ buildscript { ext { - kotlin_version = '1.7.10' - about_libraries = '10.4.0' - hilt_version = "2.43.2" + kotlin_version = '1.9.22' + about_libraries = '10.10.0' + hilt_version = '2.50' } repositories { mavenCentral() @@ -13,14 +13,15 @@ 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.2.2' + classpath "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin:$kotlin_version-1.0.16" + classpath 'com.android.tools.build:gradle:8.2.2' classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" - classpath 'com.google.gms:google-services:4.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.4" - classpath "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.4.0.2513" + classpath 'com.google.gms:google-services:4.4.1' + classpath 'com.huawei.agconnect:agcp:1.9.1.303' + classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.9' + classpath "com.github.triplet.gradle:play-publisher:3.8.4" + classpath "ru.cian:huawei-publish-gradle-plugin:1.4.2" + classpath "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:4.4.1.3373" classpath "gradle.plugin.com.star-zero.gradle:githook:1.2.0" classpath "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:$about_libraries" } @@ -28,13 +29,11 @@ buildscript { allprojects { repositories { + mavenLocal() mavenCentral() google() maven { url "https://jitpack.io" } + maven { url "https://s01.oss.sonatype.org/content/repositories/snapshots/" } maven { url "https://developer.huawei.com/repo/" } } } - -task clean(type: Delete) { - delete rootProject.buildDir -} diff --git a/gradle.properties b/gradle.properties index 38603830b..99305ac50 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,23 +1,14 @@ -# Project-wide Gradle settings. -# IDE (e.g. Android Studio) users: -# Gradle settings configured through the IDE *will override* -# any settings specified in this file. -# For more details on how to configure your build environment visit -# http://www.gradle.org/docs/current/userguide/build_environment.html -# -# Specifies the JVM arguments used for the daemon process. -# The setting is particularly useful for tweaking memory settings. org.gradle.jvmargs=-Xmx1536m # -android.enableJetifier=true -android.useAndroidX=true -# +kapt.include.compile.classpath=false kotlin.code.style=official # -kapt.use.worker.api=true -kapt.include.compile.classpath=false +android.useAndroidX=true +android.enableJetifier=true +android.nonTransitiveRClass=false # -# When configured, Gradle will run in incubating parallel mode. -# This option should only be used with decoupled projects. More details, visit -# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects -# org.gradle.parallel=true \ No newline at end of file +# https://developer.huawei.com/consumer/en/doc/development/AppGallery-connect-Guides/agc-common-faq-0000001063210244#section17273113244910 +apmsInstrumentationEnabled=false + +# https://community.sonarsource.com/t/sonarscanner-for-gradle-you-can-now-decide-when-to-compile/102069/2 +systemProp.sonar.gradle.skipCompile=true diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 7454180f2..7f93135c4 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index d7e66b5c6..e6aba2515 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-all.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 1b6c78733..1aa94a426 100755 --- a/gradlew +++ b/gradlew @@ -55,7 +55,7 @@ # 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 +# https://github.com/gradle/gradle/blob/HEAD/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/. @@ -80,13 +80,11 @@ do esac done -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -APP_NAME="Gradle" +# This is normally unused +# shellcheck disable=SC2034 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"' +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -133,22 +131,29 @@ location of your Java installation." fi else 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. + if ! command -v java >/dev/null 2>&1 + then + 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 location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -193,11 +198,15 @@ if "$cygwin" || "$msys" ; then 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. + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ @@ -205,6 +214,12 @@ set -- \ org.gradle.wrapper.GradleWrapperMain \ "$@" +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. diff --git a/gradlew.bat b/gradlew.bat index ac1b06f93..6689b85be 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -14,7 +14,7 @@ @rem limitations under the License. @rem -@if "%DEBUG%" == "" @echo off +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,7 +25,8 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -40,7 +41,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute +if %ERRORLEVEL% equ 0 goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -75,13 +76,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal