From 2f749a690b4293a802c6fc00063c41a42c5008c8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 8 Jun 2023 23:18:23 +0000 Subject: [PATCH 01/38] Bump androidx.fragment:fragment-ktx from 1.5.7 to 1.6.0 (#2239) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 000cc09c..2231da52 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -207,7 +207,7 @@ dependencies { implementation 'androidx.core:core-splashscreen:1.0.1' implementation "androidx.activity:activity-ktx:1.7.2" implementation "androidx.appcompat:appcompat:1.6.1" - implementation "androidx.fragment:fragment-ktx:1.5.7" + implementation "androidx.fragment:fragment-ktx:1.6.0" implementation "androidx.annotation:annotation:1.6.0" implementation "androidx.preference:preference-ktx:1.2.0" From f20ffe44d5b16c9954d9738b8f44214968f27853 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 8 Jun 2023 23:24:27 +0000 Subject: [PATCH 02/38] Bump kotlin_version from 1.8.21 to 1.8.22 (#2238) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 69c0905a..c5a48ec9 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ buildscript { ext { - kotlin_version = '1.8.21' + kotlin_version = '1.8.22' about_libraries = '10.7.0' hilt_version = "2.46.1" } From 8913b22a200685915c4da82a10802ab5ad0a7990 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 8 Jun 2023 23:25:46 +0000 Subject: [PATCH 03/38] Bump org.sonarsource.scanner.gradle:sonarqube-gradle-plugin (#2237) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index c5a48ec9..757910b1 100644 --- a/build.gradle +++ b/build.gradle @@ -20,7 +20,7 @@ buildscript { classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.5' classpath "com.github.triplet.gradle:play-publisher:3.6.0" classpath "ru.cian:huawei-publish-gradle-plugin:1.4.0" - classpath "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:4.1.0.3113" + classpath "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:4.2.0.3129" classpath "gradle.plugin.com.star-zero.gradle:githook:1.2.0" classpath "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:$about_libraries" } From df8849639b3baa8826eafc3a2c1c7a8eccdc510d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 14 Jun 2023 10:56:32 +0000 Subject: [PATCH 04/38] Bump org.sonarsource.scanner.gradle:sonarqube-gradle-plugin (#2242) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 757910b1..150bd4cf 100644 --- a/build.gradle +++ b/build.gradle @@ -20,7 +20,7 @@ buildscript { classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.5' classpath "com.github.triplet.gradle:play-publisher:3.6.0" classpath "ru.cian:huawei-publish-gradle-plugin:1.4.0" - classpath "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:4.2.0.3129" + classpath "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:4.2.1.3168" classpath "gradle.plugin.com.star-zero.gradle:githook:1.2.0" classpath "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:$about_libraries" } From 03cd3aeab78cf3c3991cb28099cca6d67d5d2f1f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Jul 2023 17:53:21 +0000 Subject: [PATCH 05/38] Bump com.google.firebase:firebase-bom from 32.1.0 to 32.2.0 (#2259) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 2231da52..46f709c8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -252,7 +252,7 @@ dependencies { implementation 'com.fredporciuncula:flow-preferences:1.9.1' implementation 'org.apache.commons:commons-text:1.10.0' - playImplementation platform('com.google.firebase:firebase-bom:32.1.0') + playImplementation platform('com.google.firebase:firebase-bom:32.2.0') playImplementation 'com.google.firebase:firebase-analytics-ktx' playImplementation 'com.google.firebase:firebase-messaging:' playImplementation 'com.google.firebase:firebase-crashlytics:' From 6e7c12a118775f9590a57e99ac6a4064116505d2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Jul 2023 17:53:38 +0000 Subject: [PATCH 06/38] Bump com.google.firebase:firebase-crashlytics-gradle from 2.9.5 to 2.9.7 (#2258) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 150bd4cf..62f6389f 100644 --- a/build.gradle +++ b/build.gradle @@ -17,7 +17,7 @@ buildscript { classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" classpath 'com.google.gms:google-services:4.3.15' classpath 'com.huawei.agconnect:agcp:1.9.0.300' - classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.5' + classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.7' classpath "com.github.triplet.gradle:play-publisher:3.6.0" classpath "ru.cian:huawei-publish-gradle-plugin:1.4.0" classpath "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:4.2.1.3168" From bb79b33b6d40d0d208b7fe546c32a04a09d02446 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Jul 2023 17:53:58 +0000 Subject: [PATCH 07/38] Bump com.google.android.gms:play-services-ads from 22.1.0 to 22.2.0 (#2256) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 46f709c8..4589331f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -259,7 +259,7 @@ dependencies { playImplementation 'com.google.firebase:firebase-config-ktx' 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:22.1.0' + playImplementation 'com.google.android.gms:play-services-ads:22.2.0' hmsImplementation 'com.huawei.hms:hianalytics:6.10.0.301' hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.9.0.300' From dbe608f2dd6aa9ab616c40a5d0b377a88d77fe37 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Jul 2023 17:54:26 +0000 Subject: [PATCH 08/38] Bump about_libraries from 10.7.0 to 10.8.0 (#2249) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 62f6389f..d12932db 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ buildscript { ext { kotlin_version = '1.8.22' - about_libraries = '10.7.0' + about_libraries = '10.8.0' hilt_version = "2.46.1" } repositories { From 29a36aaf6ed30318e2a8feb4666a0739daed822d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Jul 2023 18:11:57 +0000 Subject: [PATCH 09/38] Bump com.huawei.hms:hianalytics from 6.10.0.301 to 6.10.0.302 (#2253) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 4589331f..54cc97ff 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -261,7 +261,7 @@ dependencies { playImplementation 'com.google.android.play:core-ktx:1.8.1' playImplementation 'com.google.android.gms:play-services-ads:22.2.0' - hmsImplementation 'com.huawei.hms:hianalytics:6.10.0.301' + hmsImplementation 'com.huawei.hms:hianalytics:6.10.0.302' hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.9.0.300' releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker" From 8564e12b015b151f3ead4b13f7ae4fb5d4c8358d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Jul 2023 19:15:06 +0000 Subject: [PATCH 10/38] Bump com.huawei.agconnect:agcp from 1.9.0.300 to 1.9.1.300 (#2254) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index d12932db..75191aad 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,7 @@ buildscript { classpath 'com.android.tools.build:gradle:8.0.2' classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" classpath 'com.google.gms:google-services:4.3.15' - classpath 'com.huawei.agconnect:agcp:1.9.0.300' + classpath 'com.huawei.agconnect:agcp:1.9.1.300' classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.7' classpath "com.github.triplet.gradle:play-publisher:3.6.0" classpath "ru.cian:huawei-publish-gradle-plugin:1.4.0" From d0819928f3b61e4d020d2dd9b78d10fd9b32d4d6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Jul 2023 19:15:21 +0000 Subject: [PATCH 11/38] Bump com.huawei.agconnect:agconnect-crash from 1.9.0.300 to 1.9.1.300 (#2252) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 54cc97ff..fdb8d20f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -262,7 +262,7 @@ dependencies { playImplementation 'com.google.android.gms:play-services-ads:22.2.0' hmsImplementation 'com.huawei.hms:hianalytics:6.10.0.302' - hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.9.0.300' + hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.9.1.300' releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker" From 88ea753fc69bba48fb367a9535e28c05e14c6b6b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Jul 2023 19:15:34 +0000 Subject: [PATCH 12/38] Bump coroutines from 1.7.1 to 1.7.2 (#2251) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index fdb8d20f..9e074371 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -192,7 +192,7 @@ ext { room = "2.5.1" chucker = "3.5.2" mockk = "1.13.5" - coroutines = "1.7.1" + coroutines = "1.7.2" } dependencies { From 86c7de6595fc6f71dbe616bffaa5474d1bdb215e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Jul 2023 19:15:48 +0000 Subject: [PATCH 13/38] Bump room from 2.5.1 to 2.5.2 (#2250) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 9e074371..f3ea03c1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -189,7 +189,7 @@ huaweiPublish { ext { work_manager = "2.8.1" android_hilt = "1.0.0" - room = "2.5.1" + room = "2.5.2" chucker = "3.5.2" mockk = "1.13.5" coroutines = "1.7.2" From 05741761a2117667d848d171b43096287d1aa811 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Jul 2023 20:08:56 +0000 Subject: [PATCH 14/38] Bump kotlin_version from 1.8.22 to 1.9.0 (#2255) --- app/build.gradle | 15 +++++---------- .../wulkanowy/data/db/dao/AdminMessageDao.kt | 2 +- build.gradle | 3 ++- 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index f3ea03c1..596393f5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -3,6 +3,7 @@ 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' @@ -33,14 +34,6 @@ android { 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" @@ -156,7 +149,9 @@ android { kapt { correctErrorTypes true } - +ksp { + arg("room.schemaLocation", "$projectDir/schemas".toString()) +} kotlin { jvmToolchain(11) } @@ -228,7 +223,7 @@ dependencies { 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" 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 87f4812d..2b4cb597 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/build.gradle b/build.gradle index 75191aad..6a378d48 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ buildscript { ext { - kotlin_version = '1.8.22' + kotlin_version = '1.9.0' about_libraries = '10.8.0' hilt_version = "2.46.1" } @@ -13,6 +13,7 @@ buildscript { dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" + classpath "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin:$kotlin_version-1.0.11" classpath 'com.android.tools.build:gradle:8.0.2' classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" classpath 'com.google.gms:google-services:4.3.15' From ef72218906b6a87d7a909fb3a561d99307276c17 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Jul 2023 21:30:37 +0000 Subject: [PATCH 15/38] Bump org.gradle.toolchains.foojay-resolver-convention (#2264) --- settings.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings.gradle b/settings.gradle index a69aaa95..af9bb737 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,4 +1,4 @@ plugins { - id 'org.gradle.toolchains.foojay-resolver-convention' version '0.5.0' + id 'org.gradle.toolchains.foojay-resolver-convention' version '0.6.0' } include ':app' From c0161f38c61dc93a338b5907b374a48280e65031 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Jul 2023 21:33:17 +0000 Subject: [PATCH 16/38] Bump org.sonarsource.scanner.gradle:sonarqube-gradle-plugin (#2262) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 6a378d48..8bdcdedf 100644 --- a/build.gradle +++ b/build.gradle @@ -21,7 +21,7 @@ buildscript { classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.7' classpath "com.github.triplet.gradle:play-publisher:3.6.0" classpath "ru.cian:huawei-publish-gradle-plugin:1.4.0" - classpath "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:4.2.1.3168" + classpath "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:4.3.0.3225" classpath "gradle.plugin.com.star-zero.gradle:githook:1.2.0" classpath "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:$about_libraries" } From e79c5d4d2bc9b7cb9359377fb7709bdc2633f8c5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Jul 2023 21:36:06 +0000 Subject: [PATCH 17/38] Bump hilt_version from 2.46.1 to 2.47 (#2261) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 8bdcdedf..dba02152 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ buildscript { ext { kotlin_version = '1.9.0' about_libraries = '10.8.0' - hilt_version = "2.46.1" + hilt_version = "2.47" } repositories { mavenCentral() From 5b2e2ffb34820e475a4553eb1795b070a4f0a789 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Tue, 25 Jul 2023 11:37:43 +0200 Subject: [PATCH 18/38] Remove tests deprecations (#2260) --- app/build.gradle | 2 +- .../io/github/wulkanowy/MainCoroutineRule.kt | 12 +++---- .../db/migrations/AbstractMigrationTest.kt | 3 +- .../data/db/migrations/Migration12Test.kt | 18 +++++----- .../data/db/migrations/Migration13Test.kt | 34 +++++++++++-------- 5 files changed, 37 insertions(+), 32 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 596393f5..f8603cc8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -2,7 +2,6 @@ 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' @@ -11,6 +10,7 @@ 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' diff --git a/app/src/test/java/io/github/wulkanowy/MainCoroutineRule.kt b/app/src/test/java/io/github/wulkanowy/MainCoroutineRule.kt index 10724868..543c9540 100644 --- a/app/src/test/java/io/github/wulkanowy/MainCoroutineRule.kt +++ b/app/src/test/java/io/github/wulkanowy/MainCoroutineRule.kt @@ -2,7 +2,8 @@ package io.github.wulkanowy import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.TestCoroutineDispatcher +import kotlinx.coroutines.test.TestDispatcher +import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.setMain import org.junit.rules.TestWatcher @@ -10,17 +11,14 @@ import org.junit.runner.Description @OptIn(ExperimentalCoroutinesApi::class) class MainCoroutineRule( - private val testDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher() + private val testDispatcher: TestDispatcher = UnconfinedTestDispatcher() ) : TestWatcher() { - override fun starting(description: Description?) { - super.starting(description) + override fun starting(description: Description) { Dispatchers.setMain(testDispatcher) } - override fun finished(description: Description?) { - super.finished(description) + override fun finished(description: Description) { Dispatchers.resetMain() - testDispatcher.cleanupTestCoroutines() } } diff --git a/app/src/test/java/io/github/wulkanowy/data/db/migrations/AbstractMigrationTest.kt b/app/src/test/java/io/github/wulkanowy/data/db/migrations/AbstractMigrationTest.kt index 18249ba8..18ff9339 100644 --- a/app/src/test/java/io/github/wulkanowy/data/db/migrations/AbstractMigrationTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/db/migrations/AbstractMigrationTest.kt @@ -22,7 +22,8 @@ abstract class AbstractMigrationTest { @get:Rule val helper: MigrationTestHelper = MigrationTestHelper( InstrumentationRegistry.getInstrumentation(), - AppDatabase::class.java.canonicalName, + AppDatabase::class.java, + listOf(Migration55()), FrameworkSQLiteOpenHelperFactory() ) diff --git a/app/src/test/java/io/github/wulkanowy/data/db/migrations/Migration12Test.kt b/app/src/test/java/io/github/wulkanowy/data/db/migrations/Migration12Test.kt index f614c8ca..54c73e20 100644 --- a/app/src/test/java/io/github/wulkanowy/data/db/migrations/Migration12Test.kt +++ b/app/src/test/java/io/github/wulkanowy/data/db/migrations/Migration12Test.kt @@ -22,12 +22,12 @@ class Migration12Test : AbstractMigrationTest() { fun twoNotRelatedStudents() { helper.createDatabase(dbName, 11).apply { // user 1 - createStudent(this, 1, true) + createStudent(this, 1) createSemester(this, 1, false, 5, 1) createSemester(this, 1, true, 5, 2) // user 2 - createStudent(this, 2, true) + createStudent(this, 2) createSemester(this, 2, false, 6, 1) createSemester(this, 2, true, 6, 2) close() @@ -56,9 +56,9 @@ class Migration12Test : AbstractMigrationTest() { fun removeStudentsWithoutClassId() { helper.createDatabase(dbName, 11).apply { // user 1 - createStudent(this, 1, true) + createStudent(this, 1) createSemester(this, 1, false, 0, 2) - createStudent(this, 2, true) + createStudent(this, 2) createSemester(this, 2, true, 1, 2) close() } @@ -81,11 +81,11 @@ class Migration12Test : AbstractMigrationTest() { fun ensureThereIsOnlyOneCurrentStudent() { helper.createDatabase(dbName, 11).apply { // user 1 - createStudent(this, 1, true) + createStudent(this, 1) createSemester(this, 1, true, 5, 2) - createStudent(this, 2, true) + createStudent(this, 2) createSemester(this, 2, true, 6, 2) - createStudent(this, 3, true) + createStudent(this, 3) createSemester(this, 3, false, 7, 2) close() } @@ -112,7 +112,7 @@ class Migration12Test : AbstractMigrationTest() { db.close() } - private fun createStudent(db: SupportSQLiteDatabase, studentId: Int, isCurrent: Boolean) { + private fun createStudent(db: SupportSQLiteDatabase, studentId: Int) { db.insert("Students", CONFLICT_FAIL, ContentValues().apply { put("endpoint", "https://fakelog.cf") put("loginType", "STANDARD") @@ -123,7 +123,7 @@ class Migration12Test : AbstractMigrationTest() { put("student_name", "Jan Kowalski") put("school_id", "000123") put("school_name", "") - put("is_current", isCurrent) + put("is_current", true) put("registration_date", "0") }) } diff --git a/app/src/test/java/io/github/wulkanowy/data/db/migrations/Migration13Test.kt b/app/src/test/java/io/github/wulkanowy/data/db/migrations/Migration13Test.kt index b0c03fb1..9ba36876 100644 --- a/app/src/test/java/io/github/wulkanowy/data/db/migrations/Migration13Test.kt +++ b/app/src/test/java/io/github/wulkanowy/data/db/migrations/Migration13Test.kt @@ -95,22 +95,22 @@ class Migration13Test : AbstractMigrationTest() { fun markAtLeastAndOnlyOneSemesterAtCurrent() { helper.createDatabase(dbName, 12).apply { createStudent(this, 1, "", 5) - createSemester(this, 1, 5, 1, 1, false) - createSemester(this, 1, 5, 2, 1, false) - createSemester(this, 1, 5, 3, 2, false) - createSemester(this, 1, 5, 4, 2, false) + createSemester(this, 1, 1, 1, false) + createSemester(this, 1, 2, 1, false) + createSemester(this, 1, 3, 2, false) + createSemester(this, 1, 4, 2, false) createStudent(this, 2, "", 5) - createSemester(this, 2, 5, 5, 5, true) - createSemester(this, 2, 5, 6, 5, true) - createSemester(this, 2, 5, 7, 55, true) - createSemester(this, 2, 5, 8, 55, true) + createSemester(this, 2, 5, 5, true) + createSemester(this, 2, 6, 5, true) + createSemester(this, 2, 7, 55, true) + createSemester(this, 2, 8, 55, true) createStudent(this, 3, "", 5) - createSemester(this, 3, 5, 11, 99, false) - createSemester(this, 3, 5, 12, 99, false) - createSemester(this, 3, 5, 13, 100, false) - createSemester(this, 3, 5, 14, 100, true) + createSemester(this, 3, 11, 99, false) + createSemester(this, 3, 12, 99, false) + createSemester(this, 3, 13, 100, false) + createSemester(this, 3, 14, 100, true) close() } @@ -198,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) @@ -206,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") }) } From 398bc513fb6e6e7284a1e1126f01af755ade050f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 25 Jul 2023 10:00:31 +0000 Subject: [PATCH 19/38] Bump about_libraries from 10.8.0 to 10.8.3 (#2263) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index dba02152..9584caac 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ buildscript { ext { kotlin_version = '1.9.0' - about_libraries = '10.8.0' + about_libraries = '10.8.3' hilt_version = "2.47" } repositories { From b2969264231a65109ed70b24c7636c0c3425802a Mon Sep 17 00:00:00 2001 From: Bartosz Bieniek Date: Tue, 25 Jul 2023 23:05:14 +0200 Subject: [PATCH 20/38] Timetable widget improvements (#2219) --- .../timetablewidget/TimetableWidgetFactory.kt | 8 +- .../TimetableWidgetProvider.kt | 270 +++++++++------ .../background_timetable_widget_avatar.xml | 2 +- .../background_widget_item_timetable.xml | 2 +- .../res/drawable/ic_timetable_widget_swap.xml | 4 +- .../main/res/drawable/ic_widget_chevron.xml | 4 +- .../drawable/img_timetable_widget_preview.png | Bin 21111 -> 76378 bytes .../layout-v31/widget_timetable_preview.xml | 321 ++++++++++++++++-- .../main/res/layout/item_widget_timetable.xml | 20 +- app/src/main/res/layout/widget_timetable.xml | 89 ++--- .../res/xml/provider_widget_timetable.xml | 10 +- 11 files changed, 524 insertions(+), 206 deletions(-) mode change 100755 => 100644 app/src/main/res/drawable/img_timetable_widget_preview.png 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 9c5abe1c..d545413d 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 @@ -124,14 +124,12 @@ class TimetableWidgetFactory( val lessonStartTime = lesson.start.toFormattedString(TIME_FORMAT_STYLE) val lessonEndTime = lesson.end.toFormattedString(TIME_FORMAT_STYLE) - val roomText = "${context.getString(R.string.timetable_room)} ${lesson.room}" 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.timetableWidgetItemRoom, roomText) setTextViewText(R.id.timetableWidgetItemTeacher, lesson.teacher) setTextViewText(R.id.timetableWidgetItemDescription, lesson.info) setOnClickFillInIntent(R.id.timetableWidgetItemContainer, Intent()) @@ -140,6 +138,12 @@ class TimetableWidgetFactory( 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( 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 624ca30f..cc48539a 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,6 +8,7 @@ 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.graphics.Bitmap import android.widget.RemoteViews import androidx.appcompat.content.res.AppCompatResources import androidx.core.graphics.drawable.DrawableCompat @@ -76,110 +77,151 @@ class TimetableWidgetProvider : BroadcastReceiver() { 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)) - } + 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 nextNavIntent = createNavIntent(context, appWidgetId, appWidgetId, BUTTON_NEXT) - val prevNavIntent = createNavIntent(context, -appWidgetId, appWidgetId, 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) - action = appWidgetId.toString() //make Intent unique - } - val appIntent = PendingIntent.getActivity( - context, - TIMETABLE_PENDING_INTENT_ID, - SplashActivity.getStartIntent(context, Destination.Timetable()), - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE - ) + val widgetRemoteViews = RemoteViews(context.packageName, R.layout.widget_timetable) + // Apply the click action intent + val appIntent = createPendingAppIntent(context) + widgetRemoteViews.setPendingIntentTemplate(R.id.timetableWidgetList, appIntent) + + // Display saved date + val date = getSavedWidgetDate(widgetId) ?: getWidgetDefaultDateToLoad(widgetId) val formattedDate = date.toFormattedString("EEE, dd.MM").capitalise() - val remoteView = RemoteViews(context.packageName, R.layout.widget_timetable).apply { - setEmptyView(R.id.timetableWidgetList, R.id.timetableWidgetEmpty) - setTextViewText(R.id.timetableWidgetDate, formattedDate) - setRemoteAdapter(R.id.timetableWidgetList, adapterIntent) + 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 = + 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) - setPendingIntentTemplate(R.id.timetableWidgetList, appIntent) } - student?.let { - setupAccountView(context, student, remoteView, appWidgetId) + // 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) } - with(sharedPref) { - putLong(getDateWidgetKey(appWidgetId), date.toEpochDay(), true) + // Setup profile picture + getWidgetStudent(widgetId)?.let { student -> + setupAccountView(context, student, widgetRemoteViews, widgetId) } + // Apply updates with(appWidgetManager) { - partiallyUpdateAppWidget(appWidgetId, remoteView) - notifyAppWidgetViewDataChanged(appWidgetId, R.id.timetableWidgetList) + partiallyUpdateAppWidget(widgetId, widgetRemoteViews) + notifyAppWidgetViewDataChanged(widgetId, R.id.timetableWidgetList) } Timber.d("TimetableWidgetProvider updated") } - private fun createNavIntent( + 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 { @@ -189,6 +231,17 @@ class TimetableWidgetProvider : BroadcastReceiver() { }, 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 @@ -199,6 +252,7 @@ class TimetableWidgetProvider : BroadcastReceiver() { sharedPref.putLong(getStudentWidgetKey(appWidgetId), it.id) } } + else -> null } } catch (e: Exception) { @@ -208,60 +262,64 @@ class TimetableWidgetProvider : BroadcastReceiver() { null } - private fun getWidgetDefaultDateToLoad(appWidgetId: Int): LocalDate { - val lastLessonEndTimestamp = - sharedPref.getLong(getTodayLastLessonEndDateTimeWidgetKey(appWidgetId), 0) - val lastLessonEndDateTime = - LocalDateTime.ofEpochSecond(lastLessonEndTimestamp, 0, ZoneOffset.UTC) + private fun setupAccountView( + context: Context, student: Student, remoteViews: RemoteViews, widgetId: Int + ) { + val accountInitials = getAccountInitials(student.nickOrName) + val accountPickerPendingIntent = createAccountPickerPendingIntent(context, widgetId) - val todayDate = LocalDate.now() - val isLastLessonEndDateNow = lastLessonEndDateTime.toLocalDate() == todayDate - val isLastLessonEndDateAfterNowTime = LocalDateTime.now() > lastLessonEndDateTime + getAvatarBackgroundBitmap(context, student.avatarColor)?.let { + remoteViews.setImageViewBitmap(R.id.timetableWidgetAccountBackground, it) + } - return if (isLastLessonEndDateNow && isLastLessonEndDateAfterNowTime) { - todayDate.nextSchoolDay - } else { - todayDate.nextOrSameSchoolDay + remoteViews.apply { + setTextViewText(R.id.timetableWidgetAccountInitials, accountInitials) + setOnClickPendingIntent(R.id.timetableWidgetAccount, accountPickerPendingIntent) } } - private fun setupAccountView( - context: Context, - student: Student, - remoteViews: RemoteViews, - appWidgetId: Int - ) { - val accountInitials = student.nickOrName - .split(" ") - .mapNotNull { it.firstOrNull() }.take(2) - .joinToString(separator = "").uppercase() + private fun getAccountInitials(name: String): String { + val firstLetters = name.split(" ").mapNotNull { it.firstOrNull() } + return firstLetters.joinToString(separator = "").uppercase() + } - val accountPickerIntent = PendingIntent.getActivity( + private fun createAccountPickerPendingIntent(context: Context, widgetId: Int) = + PendingIntent.getActivity( context, - -Int.MAX_VALUE + appWidgetId, + -Int.MAX_VALUE + widgetId, Intent(context, TimetableWidgetConfigureActivity::class.java).apply { addFlags(FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_CLEAR_TASK) - putExtra(EXTRA_APPWIDGET_ID, appWidgetId) + putExtra(EXTRA_APPWIDGET_ID, widgetId) putExtra(EXTRA_FROM_PROVIDER, true) }, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE ) - // Create background bitmap + private fun getAvatarBackgroundBitmap(context: Context, avatarColor: Long): Bitmap? { val avatarDrawableResource = R.drawable.background_timetable_widget_avatar - AppCompatResources.getDrawable(context, avatarDrawableResource)?.let { drawable -> + return AppCompatResources.getDrawable(context, avatarDrawableResource)?.let { drawable -> val screenDensity = context.resources.displayMetrics.density val avatarSize = (48 * screenDensity).toInt() - val backgroundBitmap = DrawableCompat.wrap(drawable).run { - DrawableCompat.setTint(this, student.avatarColor.toInt()) + DrawableCompat.wrap(drawable).run { + DrawableCompat.setTint(this, avatarColor.toInt()) toBitmap(avatarSize, avatarSize) } - remoteViews.setImageViewBitmap(R.id.timetableWidgetAccountBackground, backgroundBitmap) } + } - remoteViews.apply { - setTextViewText(R.id.timetableWidgetAccountInitials, accountInitials) - setOnClickPendingIntent(R.id.timetableWidgetAccount, accountPickerIntent) + 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/res/drawable/background_timetable_widget_avatar.xml b/app/src/main/res/drawable/background_timetable_widget_avatar.xml index 7f64c4eb..48298d67 100644 --- a/app/src/main/res/drawable/background_timetable_widget_avatar.xml +++ b/app/src/main/res/drawable/background_timetable_widget_avatar.xml @@ -2,5 +2,5 @@ - + 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 09635758..510c70c0 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 @@ - + diff --git a/app/src/main/res/drawable/ic_timetable_widget_swap.xml b/app/src/main/res/drawable/ic_timetable_widget_swap.xml index 2f91489a..09c50b4f 100644 --- a/app/src/main/res/drawable/ic_timetable_widget_swap.xml +++ b/app/src/main/res/drawable/ic_timetable_widget_swap.xml @@ -1,6 +1,6 @@ 8saiXp+{!b3nnAW4XeC_+F$CH?#P00Vwgl~BJ6{sm_zuHgs)fq?$+ z2NEJR{S){lq@$wPSBQ#n`~&cZ_ohNJLJ$zuQ3%i9p&=k?&Ll*HlwBc@0kEFv&O|Wh zg$&?F{&vmdjK~qYxxCD)!?$*#cr#4EZlX#J@lMI&QxMcaX^rjJ$hkWiORk zu9I1jdh!H{Fb+}3oK>9g-#wvlMN2FOB7n|CblW^*u(L@Hu~K2bcY{87r(l0S57tWV z{Qj-6jhGbtKasG&`AgDdNYcmpNWV@^uDD%~g{Yi$SMiyCL~EHirC<0;Q8aBxznP zRAI1vq7C^4eyH8-Gop)+?23_QuRg2OCP008W5wz~(CCm;JBVN%K$LCM%Po)cwq~@i z&)3fBqrWvR2ua@uek28^t#EVtQoJr%^DR)?C91`})SyLJtVi}3r-XAB5=QE^jnt#( zl9c`4k3{ewn2|PV`6qui4OVGi&8tF5NnsBPLsR}HNR`_fi-$t#SG`Fgl%zb=jrjO} zT!iR7#}sW6K3w8Intu(3te%s3DthiFR;~(@`-#9MS;0uX(v3lcR+(bu#}$UeS6zsl z*8M7P)IL~qO1v7Ov_LXn3bQ6ezUoH0uhqVeYX7xjV=`|hZUC9l>9s9-cY=b(-oNsS z+Skb(We>-rBjI=5%>G^q5$f$V1-%QRBc192-Je=l6tEEP!nc1_y{;}2LJj%Oq%CNq z3f;q$n7s_|mD*p_G0kI6bPCmcTM}(I&4_1)OGt0s^Ps*1>JsxW$aQ-tp48u{IgtMZ z^JjrrP}UY}ME^~3d5#y8urtMAle<3!eQ;5XctCwe$S*n>ModmY)2`UVWsRFB8;4aqQxKTd+4-mjc%C5_izeW0J&t;MbUu6|g%S?{?Nz#cXGrOahd?ap=)wtUQ)rYna|t zCviD&wUHfJaV)>9$mD+?5ZLx-xVhHO?2a+oJHA*OT4v1^!G!>ghBtK18 z59h1aOKVSeI@~75ASUvXKZ)Fqg%!#Py^2&qTD?km`&0yvvA%?&r-g(i@cnkBxuD|) zAwiti)jpG;`BUf{snN`{aC|j|vQDMn^qOad<{WmPmzsVYkczKY=(H7`(7ic9gU=tW z8N*q%53?=Y^k+2jSF53RaECK{yu2EX; zy4L5>Il(T!8UTQ3Y!gS!&Q@=9C80K7JfF~87I-y=9P)0W32QYbYc)o|Y71!${$dT` zoecnn0?EZ@ZT6PtbthBclw0$EqS3a1dI-oyMiV-}+F!g}caWd1aqjc^faDGN#^bwV z#AEbk2b?V)h!CTr(EQ~u%<*;aKYNAhSgQVK7a#DToj>ri7XhlN+?P!FeNk|%ZsrW zE#JXSB{1UmZ({k&^Xm&k?anSx{c9%)zqDrYVlc~4ntV&(DAZV?4MV}Sz~hSk>Xgt> z6BicGrsbQjk!9;ZiE>$=!L6(NZ$k&KqIoZ)NVJ0}$EmAHqm`7%`To}=?%FwWE|6qn z94-EiusSqh)h_FGLJErP#7$OuCbXfMLPXplq&uG1WfZrvG3g?5`zSTDMogo!e#T;- zI=)(Wo(RlM^TrwBkDISK&{7xIH2$IvyK0D44agv zPv^#RqI?79Rv_nZlwOK|<0w!_y4ATK#M1fP(7u1~+S2IUydSqF z2F4*k`ba;cUJXP|-I5(htN-e3eH8p}h!<`2XqMTBL`qtH#*8OEWGiiiX{KffV(fQc zg?sHTM$N4SS@jR4lhfdn4t-udj^q{=h*b!IGN$Nfvqvv)08A&uLDYL6@R+BeUyw>! ztD7)Euc?V-OP&FbH{y6k6+n+kCkWBiaih>qZWDSM8}8YIv!W3`Q520CTNVVDhyC3R z@G6OT4yGe`$v~>siH;YRI zY4#_1Cv?W#G2gH_1L&;t;s4Ji>zDy-a%Cx97TR=?`I2y zReX9NsgFXr+#Q7Ud13$khw8~;I}`Z96@-6+J13?MXm&waxh3k!%%!q>0zs{M-9vx_ zE~!+I6SYdwM++7Du%3rY((J%u!QZfQnf>!n!S;KXn5%B*ABBX32LGN#;cKwwN*2c! zsWxCt7f3`ba(f=KkLxuDHBqNiVTWSVejv>e!IY_zP8-kao`xO-#Q@noj>SLA`=pVv za>kr2+Cpq}yT&Cq-DF53gDh-s8{FwP)(Bd^Ps!;7h73L4=AB=Pq-B#u7k=d^9bh_L z3Sy}BfnK`fCWZitJ^yT?&X>)>Q!Ni)NaGZlA5HlJM@^3?CY#k4iv zO6#LU9TOzPrBPqwbw-rxzzLt+_$dF(>##@PKMsQ8SUU`4nK32V*_D!ijxE_bd!ZK& z6Cld@wvNJLJ!Qb4-aj{o$2Ii`6k2{-LP$(hwOR4qJ3J!d{(*l%%Z`}q10>LaMG0e7 z$L06BMKm-S7YY{A+jtz)`W2GK2{J=RZH}$lNJ55|TEE8mQqlW~+4yCtN5~tEsMi>6 zvS{K9M(O(KVYS#o<;GdI|M~L)x_pmYF(_H0uQUuELM}@Hnn&$?1Y5n;n(EP;4NZmj zt}8l3sZRfl-sKE1pk=YH(_{i5XHRHE!Br^@<q_hr*gK4xvO_%aI;t&d{^zdNc_2lpwOHFPi+S&B_v@vSVo*{#?8k zK+dQPK3jL2o5+mhb|>hT221Er{$dC&x5iGuzL#?JNDL3#xNfsw(_%K`hv#|||2acL zLBgM$NQa`JR9ds!qf6e){o%+vWz4z1vw>2{?N-q4ZSyd)=vYD3oiE>SqLt}Py}16+ zo@;tHM7g(xT$|76FHX!zM^&!2=vcOBRft>^b>P0sgfSCZqIPL0b1n4J`iOc!;BS11 zrqdO_{S%UHvs$ApjP_EJf&KUedUbWImHB#CstlhW5&QNGilXf6!=?V|ugW@fdnrL+ z_0KWGZHbf6-HYu)B7?K6&RnJ1{$HhniIsIN>m3SSY2DL`waXkAn`s$kglV@6Xyp$T zwa0=#-VID-HM$?CdAEa4Z&HA{Z%D;9nva&?!}R`Xu9)f_8R3kGi2EP?l+QR&1?`kI z{Xd5&Yv6)6r=fQlRba*hfG~u*Sm?l#!~T96Lwl=jP5CWxu+I(ZSeZGjZbcz7xaxo+ z-lt9!BPVar> zhGW6^rb~4TkN4*4jso|F0q-uM~%t+hE1&~Th- zG8XZxiSBj}Vu1`^pP@tVe6+rg=p4gIbE0sZ%l3aiqzjdVgM(L%dznc0-=}wY`7&|1 zeOLR9sVzIJ36=^l`+O*b>VSz@7W-R1R0XH6dL3QQaN9XZiN$U)^cu|XN9C9;SE)aT zO!f&^p_ugO?@oWaFOoEFt?Vb(kv)p*3G-<9{*YWL^cch8e=|3wb(cc~P%eqp3r`DB z1;T7DbS5n#CVquMj762iyFL!fG8uDhvR&WBYpi-2RG2yG-WFRf%F&C=3SWTn4iZ$W z2*IF7s%#Chuiekq$U>=yw2dukMJ?&kVC_GKcZvBtKWDKdH<2X>4&JH*iE3W5^oKW*cHdX4Aq7-Jv;6-osH^7D{jWF5Lz-9KPUaYFNj5 zgpj;GvBBXjF!=Ayjoqm3ZI>r8G|b6wNK3G*lB$& zKNGVMg)`yj$_PE2fbbwf-VYO50+0t%e}?7vt`CkY=w<}kE|xx8!y`zs4x6^ZI7bVb zBIjMm#w~Re1B0(?Q+Kd}muJJi!cP;;}6VIe8 zlGd>$(dCG*mcUhPqTA`XaE+UiY4FFkbGy+tn7CMCciYfea6K6tJle%$kdwZEUm(Qfk=CLo)bwcjoz zSlJE5?55itCbl%m{DZ3SY*a(6s1kKB>>=g+yKWuYGwo0jj9ai-unx63UML7wxIke`U1!xl_Conp#)62oJcvt=Bm|r zU4$Qg=ztaa@Qjn_^+aQn$qzN1^-wx*AtTP9LZ`O8*8+C8A+s2_M;+~x#ct@WbH(v% zYoC_)37pKPyAChT3dg&2U>|>IS}go#^d`KN$kR8Er*yUm$G!K?+slNN%MewJV=KK0 z`Nc^rXErK2?1f_hz~`l>$FRpl%N}ZNO@lQUcBoUoxHPtlKhkS9_nMOIF_dTbWhW

i|fDG z2Nzd-oSQu$FJfR!)jc5oKs5?;I0J8PB)+a0H0qP61xP9g)Y0S=u2Vjbe_&_RK&&J6 zV%{%TM-tl|%hbVR;rzy~c9>Ue2>hU=m(HDa{ivhLXbi}D*t$7dbi7XPt<-Du!mbeg zV0?PYg5rl7*w?GftkAwcQz&PRd~7sCl@A0J)c|Ry$4sL4NXUd_T_>^aF?#O zPj#$dxKo539`?#rquSS-+k1frJJ?2Nx7Ou^Y3){loX&w{Esf^i?~<%U`Y!kDTZnYB z_|@WRgkhBF6;=X z$TGel6u#&Ls3=kD%QrHfOBn-{)gG<9LEAf@)VxD}Ep#xeqf8*>Sf z(qfrjMm6dYKHiFV#Up&ea5;7?7S2ztuYF2SOXI`6CJNWupV&Cv&Es(>Bz%6}U%$5; zgK3?B4G!3tii5BgkV>iOO}yiC`LdllXD!fpb}v!-!~;(H=cXIz(r@dEfbbm|muUwb z>;nm)wEhRW+^v|NixN#-J-yPWC^AIWIAw9A?e>AlwA0F!dRWIqMzJK-v6GIy`Ha@T zpxp&ke|ns6$47tRjo=fr1c>rV&IhXO*S#@R6Yi#n+KJ2~mCQ3422xfv=gI>s{5yLB zEf=IGD%aa#)9#4Al!xvLiR>UwQ^`K|FInQ!F);0g6M(x3m_o;qx;flM{($TuY_=o8 zW~M&<#CXFF7ByJk=5%t-xmwEtpM99iO@{t<(@D^t6ME@me z))&37>~x_*9@&PF<;i|Dk%ujGe8-*`9%=d>Ej zPqBxRV5=1h#7Rn@8c+Xt+4SuUvl&ZYgr&=JodIGyzC^08iUp31wbnNOO`| zVMbRU5EeakMWN?JVcSPK){T%88$`n}5fjG;hVvt>v-qJ`_FpO(n22~V2nIl*@x`Mu z7$bGZKP?B4x6e~CiT7hoN{ZV~P_(y-OTrb0F7K{{JJNaNTlBq^sWqgJTK9i>L=kEM zPeiEtp)U}+Yke)&zEnRAEKi(fi3Ci3Vzh5FVoyc}LP$VXG zpoDt|K4}zl4n8@-SV=GKOUFY_K$D@q+8Yk2{>fcf1 zb9D2B(Kw#jQ;x7NG|ncfQdN>b7j}Qi-PK|qROxb@Dqj#fd3{Mk_sw3l(BT_Bp&-1T z>^;RWs$p#r0$kReCxSKJY;WKPFUG6=QDjr`19Nb?`30Om&{7Y&U-L0d+~b&urw;55 zWnv~(q&*SqwYzBrN`d`vh8DaWjC4&{H|qtn_Tl8z3zi{p7A}TRu*m?&0fQ+*acs=% zp-rh!=xU;)y9~vhn7c(1mRjR8=%7#($WVvvWhv3hD13bUv@nzrrZuS8k9g|!asae zK{oJFi8b#wTgkMHkWMF$nEE@g(HQo0mFp$Eu~-eSI31(JNMNYPty~OEz+>qG0rfjc zH@EYedlRT_sdh=*YqOmV6XP56>9Ks>Gz8|n^~Vy zu3w+#X#opnB%lv0N?<8=`Dx)u44%d%j{vuruSI>?DeP*if12q8u|OHn_;UpTzuAp? zzh~g2S=TXBipUOa%TONYDM8gJXRBrTw9*(fQE&ne6aFbI3~Nj_ zZMG=Vfg-d!r6j3VKsM2KsUT2OtZcCj@Yp__=$DmsVqxH`%~KB<7LJ_38{vq})U6=J zWmDaf#bF4uGm(HGA_5lz$>#RGYL+Nyu=8U!VO(aBlI40(2U>TW4CSXx>dmT_segZ&`!MuXJI>EcIGL|~ z3Xyr2w2M@ooHT{0-E5C;iv)r{`fj7btPkfK+^wF85b&3S8&6mJm=HiU?#B@(GP8Q0xIUs%up4%b zY7O3QSRf`#K>6njuxN|gncN|-wwv)Ut9Pd&O+M!`ZD97XqhM;)3%3gpY4Ky>qWS3{ zn2-p7&-h5C175ja3{v&(NM9lirlK4qLJI98N{Y1dbLI@se7Inqq6nidox@Zsy1^+s z*eY7R_{woplQRW9f_fNqYg_o;(XNWwt|cFt2tRwHtv^509d-g>`A~?e-MvSDg9Z~A zN`=pE&Vt7NK8=YZeiDF5yE~0?&?#pBVGRJYdEvNkc#-P-L+@&%8|oTpgOLd>z{O9Q z@F%g}f|xdgO#8QbjW>Ef5Cq%vRXF!q^URCwD?+>Iv`DLP91Jn3Bt056?=9X(>n_jR zza)bTnli@r8btV7cdCixxaX zD-?<9s8rMA9e5!#==?k?lq2cLSZ#q@!WtDxkhM)CytOPAc>` zIJyj+qdflpmgQUEZGU-YNd&%nieZV zktgw-t!Eo-?jK7p?a?64eE)d{ri8~EUrd_HkmpI>&c*v|;eQNp!iH#15lc z-?jSV>cgbdhW;~<<4ONerGY+$#qyrn>1Ca($$4V(GnI%+wvXOp<&QAg;;+ywz%-_M zg>OU6XDpoel62Ve@6+ScG$L zVp?Y|j~#iR_k^dZUgx>4BNl1FCFW$c{K2*;j7&tN<)ciO=Woz0t9}b|F*}?3AzoVF zaPh~DvJuCkvlEvjeK0Nh)RF^z z`F(S7vq-*;8lM8{8j)xc8)zuMl-TS(fcOZZcYO$%^$GDK1=-Z?$t)z9!0|vwqDUUo zCghARx`ttw5xRy~9FXiOq+4;l=_c3CY_5~L(*<_8(H*Jpkloj3d{s3;TFN(yXmfQu zVzKsblq((qCvTL^de1OX$JbWd{$9Y$YJXe*%9-JQ&fikzc-PDQp$KR}#DSWKG?hLx z0`PTTo(eNrY#i`Wg|2j1-cXrI56k_M`D>iQLs7ptKu-Z(|`l3LN_?g#TvykW@?=XsZ^zV+}p7^L{v9V5T0j682Ktmq!D{M3mDdkk%jlVj`RG1GnUd zc9CR{H-4q!S^RtsiS!8TP6sj5OdMa86IjnLgjaceu#!;tIESflJUQ)Y2RnjMh_+V~ z=$h{Svb@U_6xMWZ%a==o`KH`ADpu%geYZ(wCOEnJzEv}2I{Bsd&Q66D5a#h2={@n8 z*Xgix-npGxHiu~$otMij(Nb+vH51J`6`Hr9kQqoX4lJ17I!4kdR2)&b^4$j=4Qr5tgx~@9gpk<^)=uk}I zrw$^&YhTm%EOsxC&L^T6>(9WM-+n%DO|gLBagp9yv zMgRXRB?l)+N!tCdKrOeW#)!inOYpxsGu2dYzHMsV@0~tDFPJw#&UDW(1qJtd9q0}I zkw-DrYaID5@XL@?;;P$!;1sEx;E`*MTC#FMI*p;gW6Pzt(s+CwxqB1FLntN`1yy}@h0o8&=Axe$7}6*IxlM2cxvlhd=pY~xlO4+Ibme$zoo>mH>Vi_sT}s1 zK1eP*U{ZhJLXrEAsN66+8u%m{D9Po^B> zP0|@G=q2-|D3Ylb>{fot2h*B03dJWi%n%MsM z50oLuh{_1yZ=VDKyv5@<8N{=ujYoeXpWVQI=Kk;ohH%)y`e7yaNn1pHQ0kfQiVL5w z3cTx#Y1M?{xYqpAf~nr5X_Hr+usJX;2?JpQA8 zO`BCaRB9k<<7~Zvl$!cO{JE#%!*A<5m*`e?i8ts%vsDwuovrSGo)YEeS_a?mfH(mq zN~c3Eh+;OQcdz`!*d5r>{3{t$aL`p-0yW>m!1!&+(dy|bm#=@sJmQ>$(Rf{rl-1$E z&iwjP1L8-8hA$wEYs+{N0SxRaxc-{2>3lA^jdgy03-daqf`|mAHZHV)m)}WgqT$Q@ z*avSM#L#w(Od5GmLTb@k9&{T}@y&<^Gs&?TBi<3N4pj5=+20O+*Y2<*+-X{UxXk@h zJWZ|AjH~!i_$FKhr&z0|x@%Tq2-8Tyo`YUk`->g8_YMIXbZaz9q_$HA)Y>id@Co!# za(a4i%=d}Zd`MF5>G^nm4oL`T4--Q<-JAk?xO&O~pUwpc1zmqq9VX896b1~;9!!%& zbP7%vjQnIjm?5(c3SV|uM#R}jV{GR|TYcN77+OP2+Hc^*n3?7`@_$iBf*shK%EteH z{7aFTM>4y^8pYZ1PX49D^Q0mXu%qXEbe_&_49}pj-{(~itMUiZezCeu@wEAs$0B;1wyyv$Qm<&g zq*YHPC|}4gUw&qrTcgD~_2dUg!o{P}*1qf)(hDOPy~R(pUtVBDbQZ+rJ$n(i&U5V+J_% zZ!lz@nCCc7e(gZZ04YO_v{8{F3VQ8V5*8KRxOdOr9l|Jt8G})26gG@3E)9k|!d&UjT3CZJw?6 zC~Z59KhO4`UIzVk$eFxMD7=EC*4h7?1J5Zn=^C5S7$Xnk@w7U!K-AZru zCe82`kdUt=%S_F5#I(7$w-+b$Q$<@`BSjIvyk_xANO)+7zvCV-RWXDUd&`BFOXgm3L&S**e(QxR>%2-IMmCn zorQJSxeOaaRlYKsx^{N!9d6^`6FMu+eSZgvK z{`_#&ckjGAlIYvm=u%hN{Nn_dQK#9WDVKI;u2eZF6IkrGOV!&kpNO!}^phhD+wa5O~*cNNuGGC^L04iPpIzdKxTZ`=F}gs4pNdqF)tr<2*KQx7jen>HHtVGrk{ zSx954L=koMPq&1hf35E`{i)rbI6qny+&I_`nSi+#)8@Xa<-2M}8zJ&xn5U)AvCmje z=JnRDt8}|P#^Jp?^xyZLsDG-Zk`pOcD-nBYIdI%Je^OAURa3p&jMbjVCKH=_%8in_ zW6Qe|i?Epcjh$4f-Ms=7>}h|xla7**3f5k7B<3bxoV5^+XVk?98}1NvuQ99hcE8Ls zvs-9GI*Aty{rwF#2EBaOwI^`7ezNVmu#;yY7omWA0O~eXj$3ZfU^0nt-W{*UX*!LS zk#rK_)vO05qx$5vCluz7)cMM&@%HtZugl7qd%SfvfzJ>4r4@bqrY?IP6qImo{dUcU z>yYIWW=?aP?IpBEh>6Qbx2Mk3+NZpBU3J>^2GAdQR&LU`%1`y0e?YmjUNqtq=%-AN zAoL16e6mjE5T(@9s=mWT z>M=cksm8uw7V}nUx32AZSTDAW)jjJ*Rj7iC9n!_Iu9Y5K)*6o#LlQ}Z0^p58$CALH z(Np()KV+EB>msncV6{@eatLY})5`T4>kUS+-?@jI0bb@y`428xs22M<v!TG)fkoT<$Jsp}ZWHuQY=5;sVaIMRQFX#mWl6 z4X)PbM&~=5v?J{{i&IDCVr`zqE6D`Of?X-K<2$7mr(M?ZZ>W*!4@(&jx*^5#zR#Mw zbAQh-wx>EwP7)bZ@8mN0OK%FphpXeL%1@IR6yAx9?^!4FtFX9LAog3njxFEGT8kT|$=4d#hc8k%S7o%>6mF9RFU(%R!gF5@% zYL&58-hKvHFzp0rTKP45=DI(Q*RXe6_->Q4Y^R#C&E1YFib`vqPzX2~)XL>iMTTVT z?(v&Mb#-46^iaru;}H2hY;2|zmM6HDZx|M0Al*o1)u!^ib;9q>{pOP#=Ci^ktA3n_ z`wtB&Q?TtbO(q&cgj3!eEj?>S9L@JnCzM`X?jqD#O;hU%w$?TZ8_?;M$j{ITdz=;cCWac_W0(C)f<~f z(qKA!82wR6u~oeKa>5rwj+GWW7+tl#L(3wO`lbTW1&*4tj;W4~K1vNOQDm z;Wd7)A}Uv0XYA}O6NLgwwdVynRI(ZKj~r5cTDNIY{wwz&AZ+s8-Fm^3ZQCvFGZ2W1 zi6de<78J2qQS29uLrbMMomu}!@eI$vDGyJS@A&cMC(^hm`po27Y!Y~-@ zEWS5M2ZdkZ!i!b3XhE|&lSX_X%O8fbd09I%n{gW)vU*_(-q`LqOOf@=N7?-g7gz0d zM|c7n06s1v@kbmj46M~-e#AwyxFTOk$pS2`kW^#RX@4?*em)0B*%Q#__JEa*X>p6q znhm$Y)%Eq)K2~G-Qg@E$t95t=eL_{Lj9n|lSu+3kL;6CZtaITpQ7)3!C0My_C8>Zm z2}2R%RVJnCm#S>EY1>fYo%L15$CMfpnIp;Jn~%HxeT=Ox2)*6Ox5_z^$`l&B|I2gH#^=T zgSb2~RLgWsuiPHoGFdILN|dRiQ1!gOyMSi5O~;dOeP&>86e@o(goDg;%rVTRBCQN@ zxQ8!YV-MYxHyzU8319F86!Eg>4jnTDA|h5Aw-=pp5nt9fsG*wq9gelU38iwW zGzD#N7?UbB!Z`4_%txTy??N#cZA-&&Z`ZhZ`|oCsCNkMNxX4V2^b)t87D-4YZ5HV{ zao~}WgscKdM)$UX9yfow1kNmgcZn71A*l?SxtiN1Me{`|^T7^FC3Bj6mm|JV=dufp zInTHbXy%pf+oKbNL@OjL67|)Z6pl)B!eUk^hqY%LLe{fd3SugR(?un!SZE&W!!D%)F|*jnM_)GkMNkT!spz`QO%@A>|UiubKS&wcyS*621} zGa;Xd$VEtklmoJJ?6dpP{IW!meQ4M($GJIWwVag(GvrXggps7)lJmF`04&O)w&{wj zIHC3C7aha^s<+s)`$D*PdT7Tgt^gtn;o=-p@X5jm=l>X$noml>f z*P-sf`@~|d?$D?htxsBo*t~}8G>K-p;UjthuplE{tW1BtOci=&ZLv`*=O1vRj|l`1^b7ttJskC{ceiWS_T+ge%rL`!sK& zQnYfVRgu%4yxLL397^*S;xRO`Wn%@EQh7>;T8)unZNJ%&r9|pIR-Hz-TJvU=?S65b z?3Y8J#~q8w^hCiEe`}jBNfvSqZi>>^=ydk+w8yX?Uui1N~9DUg+=o)*Y=w|ob*By&%u5rQ`RBz~fQLQORWqoka=E zKR!xycB+K!eZN){o)XT8Zv&cOhZBJa5+~A1Rjl*m^d=yo>;*h=!~cL!WoyXp@?W7v z4CQ0c3(2VDH29guaE%3|6u5mLcP2~)QcCaymRj3vCLoVW3g3s;Vm*~v6%Sr%nW!ZM zv~Y}~_pexHQgu7eAUk4K*aHWz!a%hK(BMNr9)nci)(pyHH=%jvQ+MlG^65*vXCT0N zmvjK-a4~+uB23wXexKp9_nyw zs-~V%wlbs2Z@aW}KD*@jE0hhqZFWS~)!hq8e$WvfU}3(m>v&WB z9k8rAp;BsgT8U#%8<&ek!Vz4ezf;dv_02L2)jMMBigdZ4WR(@SD!L@tI8Hm4f^L+` z&bDn{wp3Ci-^SVTXY9FEsR5l4FHU*SC+HD#2cw;ZU-V*K@dX0!S!*@3Hl=V!#~uv__-ha%tstc^ z8a_ogtt_)axbW(ztZ-@?f2l<>wVx4W;T6hMf^uZiqNg8By?>Z?BxNdB=p!dR(Zig6 zOlwGy3%5|NKINj3(E;`}4z)07mA{+J&fSZq&1GIrYxwPbqFFFp%s#PVlI3WXMEh-+1S^C^x?y=>!n*Q_OMdDRv7%L z-vw^`#qOe3kDBeFJd09U9g`m%X>bd>qbbWOi-u)$&k>*$jU&0aiV>NO@QsS>eBTZ3 zci~q|RpaYZY<-TZMxEpQ5fDD#=2-dg?$>T0ss}tKy}qV^%Vy44!MZ!C7s!aXh|dMd z@nZN}Moe!6M=US8JxuYWqUxELPwb|=!PS{lD*zk8^Q!{(vs@P5LA7h@yR@_dt@`fY1G z_-jM74%3uMcAw<9cb_5`Z51N{%X6jXGgO1O(GW(*{&i=+n~P2IYmAS*t&1VpZIy|_ z5<#VaBzQ@wFmu!$wz6uonuR9a9TT?&8+DZT2@}5Cl06mRe;1$~7^D#e@A)l|(@>B> zf!fELJLk8iT)4#n~(CDwT;S1}?=4r%dh6;9A7#I@X#G^dvgTQ2my?^%>2NFxMeKJC#kFpuf zc0y_XHl6UleFbI7{%*#8s~}@ps&sy z8qGWGNT%%lSqy0T$!pJGnd`PPQ#e0%NMHC~+$t(l-KXIiIa6P9rYHPh#&Rb1*Api# z5z^5t>rF7gaU+^W`&7VA9w9x*84*(oNBf&5{NRR^C6~>#pT9lfJOFkK$Y6YVS9GjU zNpvyue&%m##g;?>klAbP^Pkx`b!BPvVerwB`C{Rq;3@0+XnS%UQ|*2!?ElgB)?rco z(I2OZf}j$DAR!2dG)R|(($X<>hqRP510pTm4N?O$l%#Yw4&B||%?ty3`RZ?<-|n;f z-|qbjc$ho)e)4?Id7by!yI5zEM%@|D*4BiiPln$}tan*1plmojE73X?k|2*{)Ro7K zgxa07t0r}Av&Bakv}x~YmYc9~bwpDx=k^*!_z|qUJ3dZkO3EU~8zeUaJDW8n=t+Ub z%J^oMj03J%e(#kgeisAL*+8|1W-+Y#SVWr|R`TRi5XhcIqDRaOU#g@igrWOCg5W2i zzQaM|aw+Ebg|iayl64`6g9D4wcqx96!>`wf)ZgL@Tz$R4QER6xGtMa~wOU`wwLoyR z+irrVA0PNyd{N%AB^Kg`aN4s_h{s(ioC)lnxwxl%&*iL%`(QY~hag19*^Drxz+s%xb8WWW5eJTzxSt&^Ce*0H@$~8ZyghRH&Z1l^_7Sox8_a9Lcr`??QS5SCU;cr zxrU$<(+rYW!1xjSB8`Dw=dDlpI(;}P29Eb_FelD73xT=w049fzB^MNwV1>oxuEjkh=m-kUsj^EmIBC27E1!rKZ-h1B*W$8~xsGU7$i zZogFtt1)iRobSPYI(?81_V2%#iHEf@QUxv!KCrUqD403j_MVYqH^@UA+$@J;S&R`+ zqsF5?_c~b-Bb0j&5=Fbhmo|^^A;u;4bNp&MeWlv9uJ$&X=-tsz3^2kIk>O)iDrtn} zL?~0N_~XU}uN&x`0aWq^PvRnWLHO8_I3|T$(vw`k4Xf&ayW(9WWKY-&=@fbqqUU{{ zdep!tsw@bY^rzqBWJpcqYsR)Cju-mkjZgX~nzH+Bkv6Qs09iRZJx@LMak!SxFC?rPfccNCo63TnLMFxF%OM{5s)va-B2A zI7RWuF{DE$u*h_fh-ad{CBmF!1ny}}#I4g0#4UT%0j#$flaZ9jx>gUSYsR%$CNV{C_pjpnN^{|l2eL`FRg z8)mG>C*QoH0Ws*=XAAA9adPtf~N9N1y4`C-+ki|O-ZSd&E^fC z!59KPz3D|2iIW>dS0?JG`o!4HOU}_*A|+VQLH!xJk+_+#hdgRN^)>VrU^~0F^_o6p zH|Sc)Q&M=q93%Mq%eWZnW8?9RCyTwSSmmc(F*B7wf`NRrL&T~({5??mpWIIKz-Xh>d)$9f6|5`KE1jz^xJZl{V50>ykD{s?Q87N}D~P+?y$C>D(cMs8mQQQnsGErw^G9+wkQETnL}i33b+sR)gl zuaT!J#E2uw$(mzf*$iGi|2~LRj8>E5t4@hymi9zSk+zR29M^H&@&XB6OUrmKX+|A_PyO}Hr%;XQn`@cbyWtwg7bei>v zeP=RRtMGBp*vhP=i@|O+?*<@>LkU^k=;93uRPxoT%<|4eXvT!_NrnSzV%Ec<(r00 zTh~XIJ^0ot3m3gk>v;SpF#MYXbZ^4FgD8?XM&sR8ec6k(3q@7Okny4;EU#4dv9J2; z^GqsPyjjQ28}gS{TTPG<3>1cUFy9lROw}PHsZ7`7$%#u9&@d2!%d65RdmPRxT zIaG?jx0bhTnjl7G3Yr8CsZnwldsh3+RLH;qbANR6UE`@gO~8`^<$-tN&BvU;y6{}g`#O&C4=L7KaBl&@ygxi#?!4Lczms0rdt|>32KO-hmJ~9Xc2K$9Q;`F`%fkjesmJVsZG}DfgBvjevb2Y!RbR#c zyRa{iUN}@Do=GzcY@0MIWL$d+f#f@sk%B5~WP6soi>l&|9%?XT43{u5JbFSfD6vC! zxQ46JJe%G6ZI-Er?WD$7U!ai1KjR1A@9;adZ zg%bCPtkmx>3stqOa7kuXDKWE4BHulsgh^qKqpJo!-vWxeMXp0x=izae%by8-3v~lx zrk=3|BPE|$NU_BT#A(-myVt9MWPLpe)SD0j67KxEH4Kf>IYCyKkKSXWRC!yd^G-p) zEiR@m=*=}}u{u$RcZT819-jN!MBNwMF1;p2Q2ndnT$^GQ9e-qr5bC9O!9W6aps`%x z+J(Ge2&B}>QcO2E?#3z}%x90Y}y%~!jT{Y_pp#& zCund!c}p(r^__tJeZ_~GK-E5?uL6mnB|vUpt$71RMFn{bI%#Ote?y&6ADlLvUumVhBk;9^ZEBX;!p zsOdH-UYj#Wv-~~P55b#VAZ%|0WLRgp+Aoh<{JH9TcTXzdlvwe%njEgu^~wM_fa4Kz zhtN>vx#AJ{MGh*?HhEKMyBxGc2rF=fsZJq_cp@1<47gDzM`jUs#??}EQ0m9^AmlL; zFN&O1x1P7wd3n}O?8sl(1HfE~?jAyZ333x61zAo!Zp+t@GyW(RzI69JTaN&CW0m0Q zWL4s3VePu!qS!`6crOuj(5b@D>0RBXmhvUPMD9(b;=fU34jh-zl~viSGhu zck(wyc=Eez0oPnv%>Ni3b|7TluL`oi9@S@aK4w+J3H0+@1f2cTHl5d3m#e78O{B9+ zGu$f^c?8hXwu5<-wN?pZE35#CxR1i9n8`A|HCoTX1c(K+IE{bWGvgho)bn4Jom{MyyftSC`x0(QUK?zv0hHm7x*8F9OIV9>DNl)cqzr9(J zI0EPU9Z@T^N4r-XMO0~w9@wf-4RX2dG4VN)F;M)Gh{Nk8o2_1uMuR>6`72((I-Gs0 z2pRvzPaRn16NU6J^`h^I(Qj9^UG_SK8P&2tD-!uCD3r@e6_ zGTx8FuKiCo#NjZ|k$cK`cZM*<^2m*E;5*ZC>y&}ql6H9dlf6 z`>lEP6?&H1zxs1bY7FL=A((y<*U$5qQC-{V`>@)m(j_mjIIyB)S7CW)8SJPa3RRGfj zFtu0<-bXc}@U($6fs)oG5+;pWf{8CL!_&~Q(fghdu`Z(qniMl{0c8p0HDTxN3TLF8 z?&OtGGsfn8F2`~oDiEhKLt0X`Nbx=zvN42Vdtvn>OEU2cZS~G3m25jot-|1b{ZM9u zXvv)SD&0ASP$4r8(iIn&j;Ucw>`BovF?Sr$+WiI(Km#G$NE|DBVP@`VZ<3<2ccQm) zXK3xLo0e?^ur5)Ipr$2#>?*AIj|k0ha_)zcpta-evz=`iQm1}ui7QW?wdoa)UV)S; zJXRA#F=7>`{9knAVt-*IzT6Csu72JWmCa_>^3g_2v}$Og4Jp@LzZlJ%QYkG|9!`1AOeo(Rv-KqOm2nY<@W@|Uy*J>tuK0RGw97ZBPh7W)L)p_P06@tUjc_?s zTDUlD5wN8VVOSTSz-}c0Z4r8m7m9tZj5sxKSBO#Eh}-VYgdGA{Oz>T^PS+dfS!g~8 z5)2!2e9qD_K37NW-v@;Ek{I5>Nw`I-tTk;K6_PnE2G7p0FTc!2XhqI=9KN%J;!|no z3=>>ay?m5IpYE$xtQ(lisAk!xkBz{6Lda@=S$T1=z{6|?kH##v1Y8&Z&_tv1R+d7T zkVYmps3KkUXFwQ9n433qXH7`LyUUpaHm6w}sUH#w>DE~B2Vb|ovkGn7juBKVq1?@< ze+u!Rxhp(LEGJ_5*i}_TZSxxyh`kgx53Y0y%^|i5FVvL#Fod@Pk>x77)w?LZJoWWb z%gEF1BQzhXrsO}}%>^Y(%BA`dhLN;ub2wzH!&F;%e1$_!FVSAO-U6y#!g5GZK_cqi zmf1t0C+)Oo^B7ycoKSKY3j+$~H9z$=DJbYU>FskGNyAKD0`kWvtAB{sUR=%AI6xVG zX2P5{H%p}kGV1JFDs-yE?8S23X&hr}1u<}@b+yp;;XNk^?NNFW1APbA-~(l+Ra zar;qYwlR(Zp0^}E} zV%|P&>VA#-Camt$twu3YmMfE~5b*_q-1%)2QPDq=cOO_&YdrZtnYRz>)KP(7UI{cS zDPLU!M)kBq%k|Zv{elp;u!8v6&8xP8IG24Ami9OCfazu-=SU=<0Xb865%c*^vn58# zh;pk&HVYF9b#SMH_sRNcLeF5EWP}`C~yQWhmY!)cWT9ylKb=O$ZR zIk@JIC@YB*mMKuD8F1 zuXlN!#BJSd1`BtY3+Y(6vlNl#Gy!U}5x7LhP9^$H;U|_J5*Ng&S5C$ZlwNu{eC0zf z!q@p{0ueaDHLr}RDXRqW@TNLHm-Y+-?4n#B6y=$%yWsGOSa95-_!EWg$s+AQx?spL z_?l@R;0!RhCoFV+a{~djN!nEWGKrf(9Y&yMHTBWzdunT6-~JBL`^F#I(O%m`WSM;&p1}?u9d(IHoFOTYMD=dFAV;-FtNhfotmnajL-|FO_@!YNl+9&c_ zK1`*lnmJltli3{q-BdN3FaPT4jrlkD#X^DC05*wbwK?1QOo?KnEOE*XSiqfW{L6eH z^DUbxdzsk)xkZ}a zTY1ld?J{Jo;uX+kK#ws=>UbWX2E(0JO`Hy& z=0GhZ)g&z$krt8w9~-$Q9D}xxLo`*Gb$;yCodY_ul&t#8AxqR1UJX^L8Cf81lS3$SEDA47c7y(=QcMxmx^#Y$p2Sy$pO)~V?T%r9b(R$YNiM>P~b(X!>)KgZyMrUSpoa^S#ACv!jtbjZFD_LKKJhm2*Fto>a!8DwK16QCOl-sHLC`i2O+ML~q9AZdD zp+$8iOZlK3p5%BdP|=U}H(-viu}iXT-h!?6PwF~x3~Hkl_22=$x}9gQ1Um_20kx!} zGmE$(^M=uZ&8ZNgcaAp?Y$}YB2(#<>(Fx^mxrXUR@Q8hLcQcm7v8?*0s7{F@b|&KZ zsKbaBye_YZMg~S!quQcat}m1+NzuY+)=Qb@@EX;cZrwc%^0B>gVV)pHwa^i=c>8&8 z1}W}*5L#0|O7uy?M!$aOdyy2ZFh*vteYN~19A88u5xrk}lU>jIxrnd@GtAPxHQNbE z(Ey9Stugz0OX0jwT}C!q_KYZo5Ql0&WjxY~~#fLJHXT(uJhK*qH1*l1}!0wq;=F5U)M zs!y(Y#J2cCLSLaZY>ieuQ!9w6gbTP>I>z+&d?{4w&l&n&lln;Fkta~aT}FsxY45ds?3$JfT&tZGD|Z%jA~ zbxA3np|HFQEF(j?1r4WdFIRo@Sx2QcF5OP0RO!^m*H8K{Yvbd}w@%hm;u+&uiPoCU zg27CE;yDvqmCaeXx6A8kGnZc1uAx^5#`b{nbA7lasM3z!nS!5yUWZ)3e%LZ+t$c$u z|B2g%Vov1Q!7-T7H*73lnQwR_?7*hUsSEoQY5i;5dj{Y@nr?Ur0cZ}8yq)U252ou! zd_5?J+tYW-C4E;CmK#O)eheHoOr0L^i~Bc+ADmWMmc>rmOsa-Ug5+w-ExQ zWka2lxK6iu;!XlnXZjdp1%6D>i@{^lK7-m?<+R=|+#u8N!IKSE5!;-Dk=WAg!L6|> zNEbI0KU)t?SfT?FR&it1r(XQAIfizqapDQ6rAR$H046jlR!0c8E~fTfl^WBsUcf(b zqrhLb5P0Pt7 z3l`r|>1OO1$!Frv$@A2e1b6F7|MSfkt(*P67H(~-?tw}53@>YvVE0anlS)46?Bg+7 z-(B>+%j??k!M4)tj&JuNL&dqoO|o(Xr2W(307}M%(w!_dQ^>iPd?^&9LHyz-G-GDG z*=YDyx4Y^Nv})S8oIOWklZgR{5F5(t()3S0UBm|}Dx+%I_}RHxTy1)5*|bPTGwf^5 zYk`q(L@{0|;UsYFEW5bv{HZLi##{S(HEHCs=*ds)8tc;(`_YOiM>`FWYdcQ{*l3KF zljWKT-6@#P`^HlZ%pXSk-klm?X`WtD5XxpiK+It#uOZ$y+CD-VUg0n~ZKF%z3R5KzP5i=?ex1OQBnr|F7bX@;Lmr|g9Ixv)z_GQ zR)U@)+ z7fxi)oaHCafe5zZi@pz#^(yK> zPbeyknks9vbikB-)oSR?m|A&NP`Z|^ZglU=jVhS~$OSakJB`^gw!;(oQFkv6D_0fe zXBtVbOSO)gUhTbzT%wlCuIU{_-XiDEe<96S^*EWmZ&wr`>zaZKyP@xEjCe@$*T@oa zGTsL;MmFX7~;&dYwjvm+7vva9R`kIPfyQ#|M0 zse(t;HLk;jdV+y@i+1PEgfm+_cjR%4L#TShx)uIkYcBQ<9C}<5kH#?RxD;*p zYKw?kE-i}+9$_L95ai1a9Hy(RZhI$KEZDYlvWsV{zgU*h0Jiwr;LeTCb%aHY%?YFw zJAslAUR&RACa{nzx1FPs?AcrN8;es|{_2H_*}!-@LhVt~bct&DG>c&-^g0-nNGhaLRRK*& zLNi`!Ei+!IdDzhmERAxewGi)(euvGMF6YjF5_nj#i0FQXRW#U4kdJi40-%7!Eq5H> zr?G>#7I-6K{)+$%Z#6Mpk;~>O&gvYFCVBGAcJ!SiNa6ZfswEzJ0GWaYBX< zIA;Ot_|1LA8x7m3$|oBvdc){Ozj&-sMcTjZP>`h`U!fWgKT;D%sjM5|1DqXEf$*>p zeM99IHyNe+ewcbm3RfXc$)T6g4Rl1Kh{tiC0)jO@tN&JV>d*Nw=tRdwlVgg4Px&AR z>%8M9j{U(B+I232t1_luqdPOAey**e|BvtuXIdeMMIYN2arPmX-IU5d_`564Y{vrl0Qf5l^I40sQZha^XU)9M976roAJF?vxqc6@(q*c@hGk zn7|9ldEww@g~AdmU_?5o<^-R4Ta6pe#WQQ+-`EQ)jCieQ<5SF(8Ww43RnNk1=I_AP zfiZu4sQi!GlEjpG?tER*CkZ=%y~JXabAHmXe(05Y)|LXFey%^G2hoojC%+5Xtn=R- zZ9p$gnvt)mvhvw<-l~8YL8Gm?k@%57mcfjUZFa{d7_j9;th<9-e^TmOiPrR`#@aT_ zlb2Ge+y@BOG(b%EacZX>-*q4BZhsoR`dAp;+SEhr1!#ailAxpC(uMK;+k9rtr!=l;61EPjfxi&P7a z{LyK<;yuW~i20U@Ak#3w&8YH2W!B3$Zf!!x#kdb@r$$rBT#z*wcQV#zDceRVKpf)~ zE?1)U)F_tESfX7GSRtc;_Ny2zrE$F7=d%+Ln9q&-7=v)i25Wj+caeY?jfX>trw^vk z0wWT=SY>1qfi$tC@w+rf8?i6Mc4jlhbKMEF5!`1n=j0%3o*iJ1St#!#z4Ojg1IJ-4 zmn}%xSC$3Qj$>1$5+E*t7X>C8q)oCM|Cw%~xlUK-aiEvul#|7`b%$|hgJwr99 zAiqS2S6L}KxOL6>G|||%`SL0WIghS+S|I4X#lAnsLxyu^IUbaNzXpuBc$04t=@ec$ z&#;$Q_I#l$oj+YmjijW2__e?18iU2}jy`pCHB_+-pxJdlC&H_#ODpJ-nF!>Q z#TVmmG0NibB2trwvzxn9-h+Tg$YgheNUpGi@i{`nIW~0JhVNpPXdz-yLMZGL(2Q87 zedci0$}`ZUZj)(2)oit%ZozgI=ZuZU@AxXFF$5Un*+Ckcu6y0VEB%>9+lYNFk%TA7 z!k&VF)GMt%IBbW|CpjpH3fQF~#~Xclx@j-i?WFS34~*ae&$PN@Q~pGqch-SrV``da z&Wg+n3~|WcH-GQAUHm=e`0f-LmA`Nf`nr9_BA+T$B*>~?;_~}Q8dXO?f(UcUVu z(Ww+NMCz#d&}#}?f_*YVP&q2zC&h@GvN2$2q)Xv&UoyiIm2dLNM>;#eDtqf~MJnOQ zff&!nt*Dqh**;}m1?%H-RL?J4SbK_ftVy2$x-uQ~|eO_{MEYma`WIgE~vggHIL?Ae^_yKaSU=%z8Zf9HS78v+2&NL(#08>3`jmE!L>#B5m40QnmB%AHK-zN$;- z-3SQd4ZDpWIc-&meqFppEgl~|nV@|c_i?`9##m#Lxl&xQ?a(Nrq$`bjw|IcnM6tMg zLu%o(m)F87-a1R&72KFtToq@&-w#_BS>@tRC;*7Zrl4IB>d9BcR}5r7mvYN}XVy|_ zqF-;3CK;7Z9k8l!JP@iMLJE@cI*8S=#*8)L8^&tmpPkGV?4&^IL?yzj1o9RqrJEL8 zP1_6%)21sw0G?qytD{4w24(~Wu(RWyzZoqhBV~VZ{5t~-1MR$eu7E6YzT~9p0y5wp zWU8{;RNmw(6~PEqO&8NZq&%7pL-3l0cV2t*!Zx`}K6WOlWt4ij735u2X)wQ^# zt8ph}Kt?9-g6PXXTf}(C+$zejXI|FL)mq}a?L=8GWJq`hk{{pc?o)$ zqkVS+&c>laC}hTy!vXIANXwPOM(5AQ1Ipr5l+%f3#ptDduj=W}&@1{;BoLDwvb8_deRHCGBQ z>&L)a4&y3HxbMdh#~h-ybmICPFDglUvQ*GeN|v}>LK~jtN2w0BovSHeHdCfm95$ns z2$f@7sTR)Vvox2=pTo0WnJNF4$LoFc{K05XV+ocGUzwR??boeEER(@xg+@f@k`kUi zhzPs!*5Q|PW=piJ$-GjUsstA(jPtilCIfX!tK`>Io;9xh{_ER0FA{u`bWAOyf> zxO%~zlEL4m1tNB@^FE~x!rC&XCsKEJ-r7}+IW!wcU-BJjYEC}fjuB*k5lTjSo;=&v ztkW4Kj$@-mVe{H5#x7Cl8n+cMgTeR$UPvBVKJp8LmWwB(K*!^ zSaVQ!hcwlVNtk_+%vMWVXwNhbuxzL>i3-uGV$v!b|=jD6g}0{RkSQGwrcIb;x*xR!qX+OoL6fu|BG&^TSNd*1Q`0l z2Jn0zbMaXSx-m_R9?4Lm$;2`#ecVZ)y{-N9i+~-m^?ddD&?`%+T7O%**I(}X(kiE+0LwzF+hdsP5Z=PwhQQy z_112Z`Aj{rvb&qOd!y=+(dE*PVIrG_XZ@qTO%vrf)hjX}d8jpF`{h*iS2iF|eA`an z7&T&cl!L8sN%)JjT9Jyv2oGK_K02qhIdMwTC2R-`)cR4?f=lV)#U_R$=WW zIta6S@A=r+lsqFL@e?Hr&Ax{{Z6iSGnpr-_PV;o7zE5A!UtFJT^x$WI9(KPXaesr9 zOePlcLOi4&3${Nm)U3c`h#`Tp^z>A#-XFQj6Q6%yQ+e~P_|PdAr|^{7^ZF7KpkV_& zw*_ajTqAN^nP|svLZGV%55J)9RZ~Dqhy1z9a$p}Pj3IWor3L*j&p=o9bEA0$LrOMy zfr&d7ZuD0ZXITuR<`V{(B|9Smz%TBute?I@3BeRBZT};dv;2nh$PonGZ;|M))<_ob zW9e(WLML1nUr;ahUqKorp)vfYk`g5|^xgF3sR-@Tb8C`+#S&FmA7m0CFcVn6 z|I_tgYqqlxHGh0~jrsSfyYJs;5cc0T+a*Ey$>1FlJjVR5@)I7O_y>oPle|HEFi`yJ z-JH$eH=vq}nk9fl_;`k%zF-kw%9|WIKWK4ONhSum|4Eeue>#p@eUmn9Tm2MUjdUOH?-NS7 z9S=sveErn4r2J42BVFj95L4OP8ag^$cgTSqy-AQRX3}4^lb)#4(yu=Bq-PW?{;}Tr zjracQ$COd=NgYQ%u?w@KZ`&2B9k}}YD0EkVv~9Gnjvt^&LrG0NSJ(Eo?7#cOo=U4s zVz}s7qCcC!OdtWZcwXOg?Nu`Qt7H=#{rQ)c%H+H1XaYO&0km(pAS(G7Y&*wNPuc%I zZ6c!)!~lURUQs{qb9r6>q)dpx0CH^F{rBI%F+T3;k6x~qd_7XM zSVb%iOFc}hMRDm~bg7E$KL_+K+@`4ZHxuBVVZetPf0gQs#< zF_A!@(Wrj<8Ef!A1zz3XXm&_Gl08rj_(uPMBbYNuj$}#MSr#@L(Bk@cww(6MnG!W| zGMK|2OqJ6zmAy7q@d6&j1L5e z?yef49xkoMBfkO%xE(!oUKs6PkyWK($}0(n@YPpDt4466In^HCqP{QI{|&XQ{2J2a zo=-F%Zw6y!yIFltturNx8RW*ZA*H=!pw4p#lfWl*F>c4F6kw!^V?U7Di$>d2}F$_l9%+ z%BbPB4BoQ)dv%$N*;b#b^vuG!@|OP276*PF(6d^?r^${!`*rT1izMjp>xogjNe5)p$&p!w@ z>+3U#_^}d%{%sR42bIEyC97k@^YzFm|MGQo5^p58ObjT%elo1w!GAY}AABgzurbLj z8EA&{zxHG4V5B(}!B`rW<01X0LbvA1u0!JLMl8Yi->U%f`7wN~g81G~Q`!}r$m#z_ z?kTeMNw6HoQZ(qV`d}9C(1+qr*9wd4e>D`TexSWtWq9Q$1ISSR_nLqAzYIXI>Azp; zXjiC$a;?7Xf@wPPQDp?zl?7f zxhJ>rJvK1!+cV!k7dv5S!-Qh>OI|>ue_{tPx$<9%p&1CqPWhXT?WM>_T%Br9%{q9Z zFhxfF0ugv&pM34Hs&X#bKe?x2Mp&}O*i*v)oz`?c2_X4F4&vvb;a!NEytB#v>h#c z+o}7;wC&0=b|(xLYxh6s-UCqbsKp>aNypNIKAIN215SVsIc^do^6uY%$z}Nb+7mof zsUlf-aE7Wrf*8;rj1{S1#4>9>H)I&x6LiIG8qg0i8^~Bv2icsS9=DsJA*hSL%r$1C zQUC#?B=*mt&R`ya2mwcd>ew(*pACg)eKg5vmC>5jIXN5K{01ZDZO2J)5!fFQpNYG{ zg6Rt|NwxMXZ@m)Vi);5s3ZW5RBc5Op(iyln0=cl}^S>zh8%IcSC^bJIk20q7Seg9_};{LZ|bcKkXd<@bSz&Ye&3e{iyZNr!dk*I8omP0u3V z*1z!05LUc&0%oUDE0FR2|DuiL4zJMp-`YsUQ@Q}y-pA|0vi+kCg}jjK1M(6-TJt#y z-~!6iYAt0aZx@T4L=uWbw%-FIF3Yd&uj0|*5&pDMdMaNOpX2Acg7yA5pi4-SzmfC1 z|5@k%LWs6@dFeU>E>RT{f~*Y=DyHl({a5LTaOpLA`oznTw4?(wCMmu_#oc&G zSmu}2?R}jF=V!`N@Hh?uOI7H1Ag^&IZDtsdv+41;BH+!1^(2fJn0U#>W%z=i{LdCM z)ODH7^M_rukz-kbc|O@qRBB})r2`ZUQG0t|ojaMLA5^wA^fam8Vq-I&4zmec)2D_5M!l?nWQGr3<_;LQXUs=y2-S0}zf8&bKRG;7I0_ zJ^b0uvmN6*LgpFfoy3;m0KjU=^`*Htg#098Elgjiv85nn(F~Aof;Ztm15sG`EeE}8}=L+{pLeYj3eq1B+)W1>7;4xCNK3f_d89)e{eCdyF z$oAUX$7HA=h7^AI^5UxzK9i|?NSyD}3{exsIK(GkJmcpMVHv^)POo+G4AQWKalUAl z(9WC|PwVAzKX6lhTqLZi8+8B~QICpvLOFv%NWVBIgrP=t9w84#*DpR6@|97f&s7OY{Ag&oGZ3gy=k;x)vM&oW?apM>u* z40@Vlb6RkqF>mk_o@Y5+ko_tQP9#+yKT~WeISQQw!!M_CfTep?^=ZN?8NFoOy8N9BjGYFV2h zhbn}-CaziMw^Vu@#Un5XWMlF;qD+t#Hqc;PnE4&3SH|}6`bWl$SINjpp|5GDjwJ=u z$#K+&zI$#G_Hj+B%_A7U03@W2T4=2yjcO{M<88ByW=*_bX zkK=2WGpDh0#_<8_aTaBYfjbBUacM8L7W1i&^+YpW*!w;K z+6SCxa)x1XoEB;fFF1{V5zwn%GQt>!&YF0Hol$<7-kCJechZMJ`_AhWJ8$EiR85(R zI%c!|1(FhICR0OjjhJ<3&Gq`Th04t=d3Q6hiu>G?3v7kF7zVHRXQ9j{9#twY8tR$< zfTX9=9|_zmf$lA)bk^^;xCNl_lor7TJ9H-~CQI#xTAfESTO-y-JGxaiDc-Fq=z%jQ zDKLZd4~40k2GxnLXU7KgMxf&@%H04`<3s66@+!^urF0PL&J>Ub_OPclbv2Y#%GDlX z?z^j79Gj@2F4yZ*`X4V?LjOk-CcBT+6$xi~DSR!!l)jw!8EHTVE9&g6;O@NS^h{*C;prBHT_V`$q`!*V92KptD&xk98^E9_& zVibfv2fc~9YE<3J`9U^RYO%gZLs@d;iaelK`PwZ6)Gb*2l*~bq8mi%DH(sC0Qf}BZ zN#)b{CuJRZQ49@v&|)X`are$_bzlGl0=v7643ddhvxSE|@CZz_07Cy(nrpyuQZ~+cuD0rwBVAb91K})!?-)2*yOo&g{*hCrQRM;o zFsbnI#wB36&JR6(4Eogo(}Qp=G?nok^PAmpux=DQ2kWa(1&Wg-9%RhqSE(=|$UQVd zo5_Awf$SntPHRYWo<2J(0eo!f{J%Zg0rcnN_E})9VOug|;Q05XqOI3kF_=9J8D%uH9#8GVkuzKF`vFCO)?5rbe zw#9Bq`tWbfEEk08umh!p+s43ueEX&RVTJ^t2C7%jK)i+}7Q5mhTMWl)j@GftS7f(N z95@fD&{`&e^D@s5^k%*Mf8OmSb!3@@Cv*qopqz!xJQ5Q21|?(>3vFZ-0{>d7vj zYK(}YaCOTYiaCS(m8U!%Lm&lB`{#EjLq1>%x3@Ql2DqJ*rz+8^MvU%dF0FFsX4FEl zL~B2aID_c>lxe<*?st_}0Cf$*GBVU`>Rs0yDt7d%O*AAWOyt&f%+6-vGwU%k*KXMd z7@&L+A+q=GswwyWWo+&3fvuhkR|X zb%Fa11ohgYxg}F6zt0U+-cYR)>bm;(DzTIUz?B!-AtZwUO|5a-v7^ zB3l=!!iF{Y)ioYJWrLqjmRS+?>(=`rFZO5BRF21sv>{6RGA}}AOL|9)UZ*uT(j~ziW!rW{mNR0UOEC2G^G; z{=A!e@`Y>*tVJ-(E3!=r&HOMvoq1jxa|sLUDi60@H+KT!dvDbnHWw5SXK4!-sL9|a zpXUsduDKtwq)qMr^j^GyE2#MCzlmz`EF5>T6OtTIWiKr-)Ox-L-haKW3aXqUGw72H zxZpzb+EPXdrczC7Rg>?B zHI>g)*xrYHz?Zjp`*wupkCmkdF7~T@Eohd78vrLbPs~(<1FyJVFp9YgS;aF=Z~ig@ z@H!w5t99Q)I-_H5$#=Wks?4M}bdpZz=Qr-(xwCIxN|?gmIto{o(S%64&uaokEN0lv z#}B6mcyhvOTV^$;Iagls*CJ=Oi@2CS^_D0ZjYcL<3jXw6S&1(^9U-@fud#UjJkG=K ze(Xmy=6LGsLbpj`>z;L_mURp+2F7Ht)JMyP3?J-m7u}=IfDp_0oSwZTxn}k=NrWwE ztKQ{hX;$D}&r4@Klu7bM^nlx%T(G|xg)a@Kx8Z+_%YZR%4PA7U%g7h2dEeiCb-%%4 zyjcgH^lp!5w!wvUm|wXk<#gxk2H*pB<=yZ79#yWNPEk; zHllCu7cB(}v^b?e3#Am7LMh(jRwxoQNO5;}cefPR0158y?(SaPU4z`v|2gNm&$Tx< zZ}Q2WJ$oiwXZBj_`>Q!3hJFm6a&=xH!QcM({&Up0bRFHCI#=}G%v1! z`&-^u9cYzX_%Ih*Ll_pG+r5o)bV_vD4t>Q1DxLy7R1EM~#s4a({jxi=+1R*Tae7@<>$IARZ5@-z{>znj)SPov=^;hY8Ld}gnB`^USdne(Urz62o_DUYO^5Ia zU2;CGZTuTj!~K943H>WjbxIPlS+X9vIa`*U>ZC*gh9X!E5xnp=VM3VoY&|WpwNG_4 z_Wt%P(-6d23FqRLWGRd|mKk)sU#=R? zRzXZ>i=lFmQIh-{l3zj^uln`MB5NM>b}Ung$yI@R(FVB*E#>YNswD-NZFa5e=hJ{q zMb%Y&`47WTrq6N8uezg{k~`~JZ97G#()7u?Vj?v zPs#Mx(e06d!v4J&6zZ~j7Q1%__DDH0F~^$0_lwgMlT-Ji^?Saz@L&wmai$o%DXniT<;r@zx351VDZk#0{HA_X#5wzd8)Ke+z6p3ylpkb={^S*$l+nqy0< z|97ZM@>k_t#-gOJZA0ocr%SaIRV7?{ou6mFznqx`T4HvpM7RV% zxRJ<7m{3wR%!thf^1YDV8N=P&roNKus3f%2>Q&B*lt3`jp``)&COgn;@2i1q?7`Eu z+^#sLU0YQFP_rLY(}k2jySBRP^--3sd91T4G*h*JQgq{6thr8H`*0<%yav;Crkb*Q zGhYiSc|!@)XI@^c8=~rmw@v?Qcs|Ede}IHe+Ch0+j8NlO7-Gey-Bp|2YkJTdb5fl@ zrHe6PPAZNpzmADuZSs00sFiXwwR7guJs*@0uV-<-2meR{%lsEjM3toLi|x4VC^WCG@)9Q6%6Zx}f$9bgIKe^>#Che!O;^j19W6>CK?eHs$2VJIajj8v zI_fDZ2R9qna?Vz4f&x;P_66?#D$(gu0|?F*{&WKBuXq)6_}*d3nX+{rpg2vu=Pk0F=BaTpB zcyl)(=99dGA&UO>b`8NdTFI?)Xn+IUE#L-ozBF20(T)B@s!x{wgYclh3dD!gKC}m` z({X(`)%X{|)MC`I03+NT0?YAwCy&|)*usBRT;w$x^{auZMvYFr9v>bh`gQ~?Z?qY72GojC+*j#q&97u)bwB+oB>l^-70>pCod za-sG1U!7_N_9P5E1T(a4pSW0W$YLGa2I!!sV+UMccoGRR8KlaUk zy+rlm(*LpUhF3A3Eu}z$8fr>NI;CjiRG$JDDUG>}_Hs~H95(t#v50q{wW`A$LZvcd zUemgh&?hhl!nNQ`1|m?Ts60JA1AV~MiH_xZ!MJcoWuGfcX&-%I_`pSn){30}Htw$n zKFBwEemrww+Zm|8R@^+xxh>1WGE*)&t6^0n48IzDarLB4<`nkvn+173Qa-!0MoIJI ze)P;7+vq5Rd^VlpWizUqEu~E5W9rx3)z70ZWj;JSvZ=9)Eq$FHwc{Ud_qr5PYz6AT zc9GX#EsT9GfnLFgf*aE+6MB{B+&p^}h0?9n^)B{@R+5hm<%lRSx^n7CWg_B{2!||j z^4I^~uT;-qHKLi-5F^fxX!%e44Jvb5cz!K^Q|dzk$NQ+&o2+ZoKAIfR(aY~HKH6^J zox*5d(fA1u$7M%A&8E=PZC?jSt|JkgloJ42K8BJc)$un&p-Bx>=ufui`{VhT9{>jz zBXI^GEP(%c$;_*B9Le|dVCWeQ;C;WAx`vY+4UBopm9IX(z3Wp4KJWbgBu9a~M$yXt zxaah5i5JTA0&`Zj!m(OI=PII#zjEt>SYnLK1Y;S;k4vI2V5XKk*EE2csRu9Rr!N<@ zVVKSWB+=Xh_*GBP{9|U{`C+JzhjGE3M{&|jtVjG*wh=c_47I4C=#@~O6o+*a#-W(v z>HuKq7xgQb9FLkq_E#=?AKgj!BRLP&XY=``H8Oq?Q6Rw`H~Z+|0G$FPYl};che^~# zZF>gZ=A4Mo!|A10bOq)_$)-uCbrkTO&E1&yH*7DmA?nbZ>fc_nQia;0p=;wiW2zmk z@858-AvQ2-8cz}274CgJO}bnkIrjOiISKeNTGZlP`fgXjIB9bE1q6jdsjZtQBsxN2 z8+CqIIlh-Xpp|Z81sT)4Rz|{O9C(v zYWJhCs|RkV$jUKaU|9`7)|Kl-K{kk+WtiTX!y?70hv1#a2XRo4{>0MaN0V!e!f@5F z_b&q1Z!+OIyS&M+O!R(Bkq;fQ0n`mrJisJh?$%DT6!|qyr}650&w$?E^-_P-g1-Zq zyotkL_nsg)K)|;&Y4$Yn#W<~aV|uiwY)J($FM|0Wn$!JeP&Ur!pnPxG0e?cT{s9Ag zuezRu>6bS}uuH^8`|&_SadtyK4C{FeU_)(GihVyYEKv4j3_8y_^)7?tC@ zwE?sjN<=(taY3#EVk8XWxv^2{Ag`0$Phd4@D_wT$r|wHSKv{~024Ix@t>9d8!TKX# zcVH9ME8As^74}y3En~;Z1ibQ}zLQtdV7h1$$vMjx2SH+nuWa>RK%qMQm3x$e76hNA z-fQyl>VNy1LsA?F`u#WW{X`|;J)9dpOae)Cdj#WE0jKi^&!M1WiqKq?;IJ+_@u@5*3BbVG)-!6yy z9moJ0y<8HtmJp@a`a#_n8~(OR&EbNTz};#^qNNv6PlH3ryF(~I0~1D<5Jlj7yIdoa zd!K*7g*#;(Iwq%YMe)3QJnu8fbyeNgO1~mTb~4ZKqcAlE)$E4U=sB{-H49}Tn&WB+-TVBKy|cKUlo+}4wwisViC$^LsAXOe+@ zjK;{{&C-bhIugts>t`y#nnB8FJ;<)+Vc1FBe7|-T{@oV=tIFHHhy7$PH}ltq@S@pJelj;f;?I%!d2)Jf%sb}Y;vjW)KS_kX0z#@y=HY~gh) zH?GU95>oFAm-_QF?ny*CMm9wg;cpvOr#;I#6E0@3w9PoD57;UlR|JT1{|pNDn;?*D z*{CQGU>iY5Xa9cm_Iu*B#yXKlHS~KOLPxpbTll?2BWGbIh^z>vXxs$0jZ1tR0au7; z0GHp?+H~GoTWYOddU`9){m0CTEg7V3e9tX4k9=L6L}5Fno!EO0ELSb8FnjoDQyK>m z+r!rVk0?_}upb%8Am-&UvFpss{C}8U_A$;9DD?Klp;9 zS+xlv9oi&laA>CT_9N2t{Rl6akK8cs_fXVe9<=w*8Kdcg=~TJ_)s)|psl~_PGl##% z!plNZLN1lu0SxxL__lwh+g`a-5(e5IEWqyRrjAx9j&~GjIue$PnDjLKA1Nj|Y2N0S zymiPn8m~ZQryG=|_hk0=n{}7#wC-MnEr^hFxTFaDqw_tN!<0)XRl(I4h__uAMw>Wb z^c0CrNZ)JP=5*VfrjPmyyOCrEl#x135?ps3|Gy@YuiK6 zx_fK2UwFBGM3>D;g0*3c@q7|xNH!0e8Je^)?W|4x2S0heccVQbeyGR=uEN*}7AAPp zPZa<0WQeYqHKn9q8c(=n@sk}V3y=~szk6L!HaPno*N2= zszr#(Tith}52`n!Bv+gFfD4q@*4Dfixok3IF86kBOw{oifxVg6=vY{7F;7Z5w@Fpc zsy=XRHHxe(a~wSYX^3=E`FIL@d3l)hf&Meg>j9;rZn$bpnd#FHUXL>sn9KhTHc1yc z=J+pLced7ByZ}$MlK=8FLHBZJA&c#GXCh(Bw((#sy$kJ~Diy5qej(7YiP^FTdn<6M z9F}@76<6SX=Ai|T%WBBUZBUThEi`AXESH zDMAb`;}JcH(&B{Z=;&CRx7xZO(Y_Vwr8|H%a2bFgl6AB6=1Ts7*VzlptGD>iJ6Y?h z(oE;^Tc(0<)NiRZs*!RP*}>&~R>o1iYcYnxSBtYqg=Dx~DqFOelj(jrR)VbN)9)tw zJpbD`E(ho?9sPeXjwc`E<Glb$Z1Pvd~zgHekskJ^Q++w=J?2%3Hj>e-I zb6tHun8u3;aeH{JUSqi;TH}Fib=t3g=rM&sFq$j!MQIbh|}@ zyQzAYSkl=UEqv;~ZnR%u`E4o5%nWA>veri;aPBNnSD8O*UQsw4EEOQ#NWdYB(got% z{b|Smx&*OU7kjxHQTuofzNmK+6URjj4(sUz^kYzKs^^%$%oz!Bt$G^*CCf!WcQouZ zILvr{U4FMjz6@!e!!%?MP#_HBug{OuQbRmQH&YpwBgwyS+0nAKYOF7>m@Ykl6ywi4 zp`Y}H=xt8|wI^O)5-C2Qn?tLARDZNv^XoxGB>|B2o;HAl zQPRo2rJ9IZv!j%aR(0|sJYEb3s$A@6z2X*Ls9N#9?wjKgZ$gopFRB#i?>trqzvZfS zXBi1tuVUH%-^S@tsA!*t6L-;HqeW) z5>Fe#QXwW$e-gUYaIudo2-Z5@YsbEeGK|t{YcbEYm&&SLe1OcJIH(Qz0;kfU0#OsL z+3j$v<37-gnpMy16i?u@(cxaCp}lSH`Ts!TTt!bOKlbp#DUx?saNlV_DgL;d-9_n# zZ%{gra*0+?oT}}qLEwxIpeb&v#*^_uS)B>)lfr+r&|`2}q;qStlMjuT=V-FMPQ$1Q zl-rNr`?_*|d%j)k_>@+i!e{!mw)`plT{ssep~-+eBo0F#4%}WlPABwc)?97(mr|*A z;V52d3_fhQ&agdpEYrR#kuioAD%%VeE|g1_xLVf372(i9BU{c zybH@=ws?lyOj(MBrSZ<;!)?pss9J85D|=a@*>{(tl|S6!!8>VJRR+w!943p(>beJi zDL}s^RAAXu090PP=HWm5J=ig-5O61`v1F3{_%$EX*!YY}^{+MD$LuS-YUcA~N+}k@ z?w!O-58vpnXPH=L04d=vxxe0F;P^S)Yzc1-CLR_i@hCF^RQoajPjIy?!rfb2x&xAU zM!9!#5KV}WvGYiq@7IoHxLTG@D@dqvGn2y8k6dH9)TP-7F3E8|gJjO-UT+r!SDZEe zIq>5L^FguWb{ZE@^0O_#^Ra2Rl2_WC$F)tAu5ZQtkwhki+t*ht(q!lh4qT1S1}sGd zalT`3#b=-|FmYHg7^)1Vl~O94yj$lMyt{dmdz{OmyQFIDCYi!kcgggZ~1 z+>GJnoJeL0I&C?TFwV?rQQ4aaxJ_R3v6pDA7*H?1fh!2IUTT?76iUQRoz<5pe(@g| zP%fDV_UO}Xjuayg)FXN$2EghD@{#vxI%jq55$S$CG@qWx^L%7pjek)_*^*9?ism!b z<@1Uf(UBRp|ALR*&N_-e+GFlw3hiW5!G3Bz=8oUz)k4RDw?}yOH`9jhxGxcye8&{7 zxF)VGiapnOT*LYqF_i)Rwd$1EjR`6}ByZ{%p4H3dY;8TqCq0_SiY@CR!pKdX+mF}H z$tySJDh%NjmEHwgY?K8fo%1Nxoud*e-rKS@Fd`;R1Q- z+hi$F*8F%e+`Ocwn$wD-5yxSl3yQ13e#VVn*92Y|YL;jnnjq{6{0n~E+JImv-l=G` z*&nn+HC)B=4_kUmQzZR6w<-`;Qle+14%WkhfN%d77Y8T%e{pfNKmLb{v&fi)%kO;G z32#Jzzwv*_I9t14I*&pgpQ#*X7dW~6HgGBj1#Nb|*HWG9OyweErXiOcKEa;flJcn|J=8NDCNi3x!u0i4OXiV!V34qh) zR*>n8#h--p%|3HmvL)qyuwbnqYK`m6tLq6pmJ~l1?r#V)n^Sw853#+mKumP}nqkr{ z9HwT}{Dz8h?cs)&2micFSl{2E%{XRtGVASRYVh}pKFmoyN=)TTBUc11^@W12y z$=@k4s&KyTN@CclI_c1U=r&Ne9Vdgyl3p&jgUxVaSI4Rf2A$4-E^({(tC!-$`; z_D3n^$!eYo3HK46Db_3`tq7N(_zq}&qhaoE)m)2wo^FRy>3@wx-T?-`&3;w$k?9*d zN*Mv+TBKGmrASow2u@@=e^-e8{>iNu&NHbqxkRJMqRE&`1Ot{iy|O~V%6c;Jq1JZ0 z_{v?Q-e!uuOW5U-U(556Hi;vm9?4~a5-3?3XCe=0>rA2QGN-gSq2~>!O{MX;AH9P^ z?Qs6%nsm2MKLe)^Dg5tn9YEFL=_A6T=glut`)b#onK3~?jQ)an#c=q7qt4%wiK;h& zJ+g@b<6WWIKT_xGVQb(`Yxzv*Dm|2P>W4onU=8CJU>EZV(jQ#!4_W!qqYQ&%B=_S8E2HV|6|L@->!+(p z84WysWxeQnx`W9t?2L_`@~I~UlA5hl9apnwMbKXy)f%W_k%w=ota+JI$~_gM2Ia`V zHFW*@+0dk=kqR+t3UPY^UH>_jdS5{xc>In%@@0AfE~P~8A!GdX(wA2Q@|#REZ+cY+ zSX3sF0bRU>X$`Jjui2C#Ba_D9^@XLK&g0hV^dYV6T@-u$T3*~s*r%+Ip=&XJh3R~7 zUp|9^r@#kPsKYf2c*0avEn2L_XqpQl3U5j66Po# z9E^=2U$~`IyEntzsp%(uZ63>$(SfqJ`7@-4YPkRUCLS6hL$W`s&4Lb5Soo?^)IPM8 z%2QP*;p)bMP*nivAU#~jRJz=Y&J7Cg-$<;e9HoKtoaz9ZGCoy`e71T6A&6SUGD+JH zmyTh+T7qujes+)TsDtWe#EOtyj6i*pH*Ape(yL_v7QGi{|Pc~k7i0h853|Lj$ca6@8qvGLy*LiE?0EF#yY56|B ztBV5ty@;art?W{9{`>QdW@vy1gpLz0T3g;JA36~kaJ(W;X99P4JSQckGasM4$i)zv zGm|#QrC8wj^w$RX&Gu;nV)zpDw?CkF?W|i#NPl8yPPgOh(A!L$yQ!aq$Ep<3A2+V} z*@qf|`*{1;(Z?mF7>-ht$K9lPSl%~5YAt6VtJyk{)=!rYy&~9-mySiJpJC_apRTM$ zg(ZA=g8t^qULVc7x$7*((Gp)y2%Cq*WVC(o7~DtvWBAHVV0BufKxf##B} zHe9i&>xz#xU4ZRFv#6jm9P}&O@~DIva#nHDB}Vb~j%H4~KwcBB8f8uwOG)@+T!ps9 zWBmngrzXK_#DigTdUhpLT2VAid_rP3Hl8aTDl=!?4cz1FxR4Q~V;h6ak9d^gqcO+~ zn|kV#0mJl8((mXBodT47WG&fWeUAMF^)w=52ygs?aLdT_XOYJMc6nN+K}ImM-ku1f zk;OY7nX)H6Ur5C%v5|_^C^@<2e$v{wEDn_IqF$g^_U#>^*|_r1zUK$a6FZX|7sY6t zBA-9-h^>}N?YV04434G%s?{GdO-R31wRq@|8w$timD)V}i*(sK2>2@LU~Y}UvSqGo&!d$4cY1}@o94&g&&(4tf?_4+ zKWK2RsBAZi7WS+35jg3!9xm3IJDgUu1TqLa`?K6@R8JLfk7J(9*^y&B0e@Kh4-P1E zX*@^97yj{3-P1t{`*FgE288``07Vtm948*CnyeXus@OIDq9xKHf75D0FG> z&+F#^fUc0)lD(aBd5`r!b@h67*34Em2~g=q)Ig48iU)3+@&gdLlidP~RbQwW-AiHC zal7WvcORCOY38px(7Yu}Jn}Xovd|mZsTp$Uw0e|@n~%5vKjs4V=!G8TPg(C%TUx{1%rr`PbZq0s5y`77NHceg47ZYrp5mqIE6vk6owo)_jp6 zEw@$tc7ZusncN+?78!Ki8kWjfIud~YmKr1wldH`ousA4e#;Xiy( zQvO!|TB8+7wBkv9(-;ZzimP%1GgTCmRs*&*prF1EC$$Npc#$RPkMZYn;A5fM4GsUn z5~=?=5ewfnT)z1J48|~NIfEGbq;WJ$3Pqqk=S$;vB@~Mw?|=(LSE*}9Ie%;$joOI> z!yU#>wfhHE|2%99n%6mZyx7SUbXBxdrRBu-qMX7?RBJcdCl>l1!~tM+cGL7nv>$gf zZ~84PT}P6<99;UVbN=#hYcmCqttAlIrTv-nN{EP;p%P- z#qV3q5=&MAkK?HMPiqf}=LM3Ek0%&QzUl$aT;UeS3Z9egXa#-iM(Gz$i_6Z8bTb8t z{;<|>1YsqrB~;$>K2?kF5;25xaa8A4?r))2NFgoZf21{r!SG*LhrQ*UO$XzH6>Sl(CsC-B#B`2(=E{hNAqgr^+`Oc zmIpP?NOjmLr_RnV=d)(l6c-T->DBl+Q!r_4W$eNaP>35i-s<2^Jx3msM*v7~Gp}4M z<@7eDSw-)`be?Tmg`4s;UMEm9AQr~X*J8qH;Cjf9&iy!FEc5CeQS+<-bb`vXBoBJ1 zH#7e=cA>fao;15imZm7bTpqSSc;`j^5<;#gHqGREU2>&=X_ga`|2OiGZws4|_{n^( z*d#x7Wn0yvPffyNTTbl{|KK=(O_{aEzI;qauYQzt6U*L!4t4W^O(06@6icI>khz^1{Y!e z;jRX-bwBOvNxt`B52T~5IihNbi<7OD)H@+a*zM4IeY=Blh|h%CO19xm!kUmGb3+4}OTbSJy<6pQbO@b3WOTtkUwl42VOwH$pvM7QAzw@`6mg2%G?NBOzUCSK9TPkXxaOB{i|DUgOq@^-kRK&gMTuV_S6 za!Hd~_bXDR5{zvU5YWKXbQbvB5^_`aC3TmccPc4RWQg+iU`zyAG8gZ6yn8kIVkj?n zwWY%03byAs+hdkVcCO3LK~8+`tHkLW8zp`t0agGtGun#Yt{?aa{S+3cCX*H(VzonD zSYEk%OCNGD@iWy34cF6BnLfM6zvN1`W=xG({c}wGC;2o(<7>l1VD9$V)vXJKIXWnD zL0Iw&1KDTQ1$76W+O21r4`T-pYxc7I)<}o zy6)79&xm4CWtN@7N7=GOX)|jZ-jWNB_<~S`Va{}|pxZOsUgXz!rfXs;N4Eo*PT^U`Rj9xHsf;gQ0XC;8DGsN5$XSzraWpfls# z>gl@LQ|t(Q74tm{`cB|P1P+(zvIXPHEI=sl-FfozttS9>?bN0%IgiK(Blcs%~wzIVVspmH5eNw{XB7z!L z`=-a0C&>&6d9MUXwdr4e*Nw9G)@0 zW2*RVzTQ50V7uFh8o1}DDW{78jnD#)bqw&j+7EQlG#u5gX&2>`+lmd0QZu)3XdO#; z$FC$X&q&?Ir7hJD)Q)9SN15HY#_|>%(%UbTTF0uF1cz3D=;C-P3zib9h01LidOG4H z@JP)`cW6Bu1DW%&1DRUZKFuY^(~ixf&q`IK#r;4=tAB6-$S->isytlv>~-_Rp$Em- z0OCt~tOhEfFRnW-6JjWp%b&_CyA-%7@>4Ua3DWmhf8A}Uh?IY5csIXbPt5cqle*ee zVUdr?t;(~W>^FM&ooQug_JU`%v?JNky8uPBZ>grP7BUZgY$aLUdk1V?m*o$2O_&o5 zy0&}kwns%zjeChz*R+%hWJvn+mOq)VS9wGPnxrO~pyVz!52w_TJdPF{pI=9QLHH4D zRhPcEQ~&~cJUZW1zAau)8OR%mY`M=0l(tvTj~`Fvd@y<7(!UcS;;!nmrRL;R<_Df6 zHr`$F*c^96_Dw3tOrQm?R618YHoCMJ6LBuPH|@ROj9QmXpv_BTDrr)9qAP$dyDZqx z>`g!`<-(C~&XO&6n~b5j^7dzz8jhwCId6rm`a98?R+!m&ffnh%Ay?pl`fsq1f{mf; zmoGhOS}03E~Mx%z=! z#p)pBz&?d`h67_-`)ExTG=5g=l^zu!_Aa zDru++U*0%f*T3q(VZUppA-ApMf036ZIdSBCVa@fo@{9g#rozT3p3E4iq&ryAK)OLb zf)AR=cYND6DTGso#hE48hd07s5_vuMnxH$~{&ReqOYg@8JJ5U#Pz3dIsljo^wF$%h z%TevZG`X_1p$e&Ib08-q=4Ai4c0dW~Y0~|AZ=tcyr#uH3|M-q;hTrPW;gF}^K<42V zmkgSAJjMjvsLuyv$Vj);sSw03P!w2f^_5d`2zOWoVk(6`8KTn`-#Zkz8-;7?3dh_@ zJZ4&>m91IzRr?O zaLsZw+#QyiucW#{BqQQ2^ce1Df=I(1%}Rf@Wh5D*fd50%m1 zYPw8tdpf6Va?j7U&KB#t2xX4xi~S_|UVg#xC>y5_JTnw^5!U2lb-%XVWpf3g=?g** zlx&Erme(y#;3H8jSEOfkU0=Gs)Gp4q;`1nMx>F0pCfnG7C|sNpOz|3O^8YMu`pZT# z+E5c`{K0fx;HlSiexFPzUH+HKX9)=ji=q;N$=uK0{ss*5%VHsG@gL&Dto0iV?iVF_onLp{$z z2XEuuiUb&a<>kIplmn?@I!###81DQ;2_dHw;L+M>wXLG?`!N5&c6d5P`Q&!!rUQ-B z5A7r-zIN!1r&O;TzvFvSlSvWam!a=ZH&ErAGO$|Vjn$rnM!dEc# z{3A!4)V!k_WD9mR^NL?R=_i1IU!I=i;pWueSswv&#u^iHp`JQ7=q&$XX~e1sO1!p0px=G#{c9f}Q6>6!fQLvvYFmbKA(x ziUMAd7sgBSe~w(X1@QwVf5>GD2Z$L_3z6lx!?tSVNQ?bTiPGEzB|}2>)J>A`LGcoW zg!A=wM+|*^eYiW5EL6wM7mbT9E66T=MRegx;NEbqpAA+gCmN2Eu5Me5oC5;MH9JC6 z2a>X&C*y%aQy;>kj>^?$r#kRNo<_q2*tN#uFB5!GKW^FQbj*LR zRzt-=uyt*q*Dq9Iuprq_iu;Rt6HT)K73C1tbm%B0F&4=gl9HKfCd%a|i{mQEL(A7Q z-k_i8fLnhybvGcft8*J(dUcf8FlQ5U$ZD?I)A-@6%7HB2=49ekC_VSs!e-A zLhBo8HNA#wo?VZe$Kwdw)&g12$a+4W~t;DDa_+$kSe?s0KBaCll$tE#nD z%+*~kjXI;mNZW}B&}83w`!`yc@f*2IUbUymf;N+f3T6aVic}Et?Kk9Aa?T`?TeX3_ zdTwfABQ9t*T)sJQw`S{wRO+4Yx<4i=R1XT#-W88;g=&N6TOeLL5~X%I-E@j=07lsd z(pPQcl9y_CYX!`GQnCV>%7zia#Ve^TdqJXn*Ta}2eG@p#C6+Dq-1d8)(;IK0DXw=A z7o3)edb_=kjw^P)ji-a}+!;g*2!`G`0^)Juyice;kfos1k> zXvkI{W~F?k@s)#@{fcXp(zt!K5=I=Gw2zFGxw0dNJ@CflX*K|lHTZ(RZ0+_g<=`aA zdikrn8i9R$831|@MI7%8TOY;k7vn*UjF8O^eZlh0 z5Ml)Qr}Ya|!|q3MWlll~p}J6&%5xgaM+6%HLS>!VZ9v}h@7I$^8AJL5fddFTQ!k$r zeAKX-x6s-pUbS~DWua}o*xpGp$-YZUQXd}uckJmI@7 zAWkCVWr~%m9Mf|KFN62M`dFw!Rp5*b$l|C^s2v9d~+ym z6UuWUCAp&vBv61(} zShP|FfX!>&{{34cjB^Gq+V7|qwTk^xA|V+6ioG(WBZ>z}yvIC%7tG`U=zUK0dWbgB zB|#DT`og1DQSsDL-~YMPRu7TQ_wj)~->v+lriLs$u-CPoZ+F%+|0ngD>{D<^%v=;+HdE69W-3#N@o$c_jh{LpuLFtlwC@OubXy8R<@JYlG! zsYoY!A7$?Qfu846S3~O5=%ptF^+oDXvR8i^nD}lV;z6MJ&&OVx(9LWdV9skpPz?LP z@g}QKBm=ubap`y9KhKAKLg|Van93{5*wK_ZX6t=;fOIBkarmmxeo<4zFP{>k(BYZh zmqH?3Btsb2m0vjLS7JO`8G{1{1*Lk!rjQKqKj6W0eD{*el#iE(j`zRcL!Txqq5-Nw z$P^Wg>M&lD6fI>5w^N}$UrkL~zfRmaQQxR5X_|npNKLD@Ql3XTUI&^?*A^C3iSqN3 znL6n?qniYQ3t;kv33tRZ5rfjB?B)6yt+BBy!jxe*ALd(rf4DNf{I>S* z4s;c}^NXi3swRshbD-V)8X7!J>2m_@`WjG01?CU~;^#~VCoe*Fp1%`!^Q2Z#D9iw8WfKo6&N3unV8o@ zC9gPPVzumv@QM+k%oe7kWcZEyoShepk<_A zUEQH^1flpu>d@(GN_g8qe?gGI7zuuo&*gZF>&F{0YKwNMAmw5t73K7s?Y{(>^mKNr zbFV*Fk=J*_@p37k4*q3Faznis`1|Cwm7?$_ z4_SR{9*vx8#pQW?sGkd1&9ezlvuvkUK{c#Zgz(e)o0LAEq=pl3sW>d>O!E>|wMr#c zNiT)Cl9crM$Xg;E$fp8HHq7%XSlh&$?1`V$yz;-Xqm_?T^^381a~owSin>jKF+uye ze`Bd?w2AKL+HHNL6K9jt4jc&T&5+oA z&b9i*#eBGxJolqjhrUX_xZuBmh|oFy$i`p=)5BV{BFUq+Rj|VDc)kjPrr|io>R(*y zU#JEy3RAy3{?n&dzi;LQwGK*plQ8Mx{hK3J1--Vwt(YJvQBG;AMLx#!xs0NlV1xs{ zuchtazK^&>%+G0FqVP&SQ$T=EawMdN#yS5o8mZ(bBxmVRN1@2nD)M!RqD^=IYXvVb z#Tr8kT^ODd^z}uSi$<7@P5Y+kBJf9 zFPq-|8z)J!;m#z>XYR9=Zct3De`CoD@%NkYaisS^d6R$D6M*u)*yNUj#sA;`X!U|G zr0@c1>;Bb=5Yl^P)YiAJ|BbrVDEP6;e+Rs@#ge5mH~w}0u&fPvBwd1km(QCG_K=ecHfy-T zW;6rrI=b>(M|P5bh4g_AUDq}G9`D86xuKr2yX#hh%GCy`2esSi1KjRcz8BIG39}OL zKGoO_q}s*dH^)qn2$YS&uF+&JU4>suDyxlZ6r&ScVDzg-`I=UAfBz>fg~mNu^-{$U zrD8%y{GvJS&&+0@I7#T2?ek5bSt#*x;VWs+Oze zyJSL9=~50<5(%}X13?X!8uXq1^_NBRGn0P09Jh0&;CF7{vrA~zn#XrN+QtQPeA$On zlIb^%ukb*Bexq^oQE*zP2w(1g(pPts6t0Pgaq7fJM6iS`Cx39=YRr-qqZn0p;k8Z| z7xueRzfCAAf4FNfyGCQY7nTl2$9;K2_F^>V=Ij`h2C`p?l=?pQ9AcEx`c5DIu=5S!eRS@hJePr2B(JLeD2G)t;Z~0go5zK4xW8; zMF;iwv@o(;&LX&ao%qc<&b)pV9^me-aXQ~(K4G05@Nbe5sZz;^w<^q1t74XgI)J5A zW`>!z8Gn)env~=|GNJ`m{Wx|mGuq^qGW@=_cBz%ar&(@YacFUHl6h$WnFBF$QMlOusH$IDc8>F%^?L0ncrozibtR?j#0zlyhItyCV~ zlqec(2h~B z5k*TY(b!inJ0b61aZ+jr=CfQ($~o**&rT<$5VHhnrJ5y?8cx_X0RwV7a@WRbp9F z9Aovj%q34??zqu1Id3R2udet|u3)1^U%q_W<~Pf3ZT81PLmX>En4d|H@tL#Xd>W=7 zypzG0+o?w-g-vMIKFGas3FMdDy$i=ZrUNDc{kVm?t~+A zq>+*=PvfJ6VJhA7uRECJobywxd}5;dG1pX^_}aLD{&AyYPKSS<)WR;P{uMIV^T~7p ztxxP{OwiGiUe6Ma)ZCV0-tC2XjYq`+Y(rgwU&qI;d0*)eDi+2LrmhZ+SB`W(^ciUe zX@UkF3BO(57fOvx3P=ruFsaLX_QxOEc&K$&5w=0FRTbsNQxd>}|x_q$nlfv$4=L7C|b1Hjh?`h)sZOt8Tv0f9b&Yat$U7Qb?g!}g+ zY=FYNCy~SMtKPAcrE$-r(g2x4P-WBI*^BKVjg$Bdw}(*vRGaZPG9y(x@FD&{KBeP5 zEl6)3Mq+Z@nWvY!?*O7Mcq~s1>JBF^bbNd*@c4e=T!j9~?zVm$>x!;yjZFS61A)Er zGAzCKiuA~yI}=?>V$zjU@xxqov)Tzsu4!?za zpDfaT_wN*y$fmOD&}j3v*48Sro#n9B)lat&$fF zOk+14j5+NVE8q6+Np>5F`T5jW4c9jD1v~r@j_>EHCQwqWops)C`ib2!fxFPvpWM?( z*Y`91Hi(?^ov*&i!be7F2OI8UMVt|JrsPb}OV`i>U5(Q{!$Lq}G7XPK?F6mL9t=9n znvsy`Fu-?LZQVuGW0+m(_2=8JHVkIk(u5_~v^wtt{^U6(Z!En=akUbAgN_!O=T_E& zaIo^Z4n-}xHd+Y06KQ{EH#Q&66i6sHg;0e1Nlj4ype@@xl7Qh!XkOD^wP_~4(3W_` z=E7UYuU@_xw(ESg8BY2kwKyGaoxf8d&(Dv_((;Jch!miix)r$#9y#hUkh-3WtOqVP z6KTc%6*yeu^tgYyYqi;xKyF)w#|wa}o}8DIZ{N;nBq(u(?9C|OHQ$D@cL7yD@Ekdf zI%GkSWRt(hTBkvvk`9X=Er*?qg+H_G>aXM_MkR60s43Nm`(YPS66p`+ zcTX2%|3dzz6Ah#pu9PlCa#?v*yO+e}{}y6c5Mr3Zyykxj!7nOBi732ojteT*;A4U) z01zC9-Q-L53WP8BKWH4+&p~GX_CHmdcps9OYpF81<;Q?(Pjx>&=2N`|THIm}^qC!$ zB3HV*L<>j0Gk{bVkx_Ncm!|bK)M$+wMUB{_s%ne9BGN&X7By3~YNQCU2_oFBmJ%~|LQ<57h%K?g z@3=qT-*x@2>-Tp~uH>Ba9?$pd^?1Hs_dV_#ls5RKOOTx=n-*}b;>ZK9l+TMh;PyCP z<@H#<0W!h+Cn%gt60H11*zmfL}YJmrpun%|5$SDpLg=TF-305BBFv;sXO;^X26jPGSOaY@?!F+WCNx7Z5T76fF; z><5*!s0P`H8C1CPSDC*LC_95X0phY#wuj|01i8FWa(8}Q_Q+ihf1bdIoR}9Rc#Y;bespi1!k}urwgnISKRd|3dLw{V3Ch8R{9|fDo`XzQOmn!lm5Ia=o)KIx& zwv651>mmqrLQ;;5n$*C;&3ARGmBAm1j|{LjU9*_(?Gnoi9ai@HW71*j(&>gBSx7!g zpWdu=%r-8fd-S;ro{X3yEfr-&6b3d^6{y7HMA@(P#>M% z{)Zs#Kpz>HLv{h&(o0bKj%hvzQ0>_xH?i?#ZesdHBRD0>yS)|q+RvIZ1Ybz5o5Wy^59uqBI3pR>i)6HgPVXt*G;`z zTqb&Fq3~OkcxgV$7PSg2pAVPD>Sdh1Kd1*LxD8`N8SH{r2#mE-USBaMb5q9j*M`Qj zvX`41za`upigjN6^?*4Vxd~&>!uI1C$2+fF60!He4!Cr_+>hi_)*V%WTr7yIy9@^B zy9`fF%L%`D#%8*5VT0+z_#wmSw0B3@Ja#V#dL2sOb%|Y;`l=QgL2(!S?r-wIJF%Qa zo@;s|0(*OrR+dJ#Dt&`es2g8~i|5=jo^;Bz5TybGt|1a%e9IlaPpjVr-B3`piZ?N# zk8GFQ^6Z^M4QBT$I(TCp(IG?Ywz%zxDtq%s%3vjCvajc9r-@=gPR0t~Dlidue~y}( z*_0d8kztCCqUTLs)f89|U7?JSR^5OGXc=@@PI;h(dvd82J>@JeYm)qxXdn)DbqLyC z*z!sac9IDG^OQ6>t-heBa+~|gSol`LbwVFg<>Nsq9aZ(SlmUKrStOpzcq}x;I)jEbr+f#X^-@`E+50ZV~t?TJcSV4!!^0@%L9oQy9s`bdcLME*n@%gvf zx@8)v&E01IsFd|iQ14Q(UxBhO*X%hmELkaupN8BZAM~TXT@W+>G6w9!^}7p&2FqJ* z(SOeNrA3}4k3j7Cv5q{>`nvs4i8@rT#6{yj5u0zkn|cOPK(%HaI#f%Nqd;nl#ggJ# zRS(w}akZG$((bWQH&Vw|XN+#^cAu>eL|ruAM6|_&bXO#J?Kt{5o*ZkC^j5~pOZqco1apMYExqZFz_jy`g+B@a+I6yu2?UMN~ni;9-ACMJA_HRR1Ot}$u z!*0GvrH&?ydO^Xv3u5VBKhb%=e7vf5Ml3{VbWZ#(LbaR>388`kOH=+`EuN>U6B{5__8JGd5qjCwM;noPwSNSbY4s4ESAu8-4uatr$W}*9R}EI96NR zG`)$`(!83J?be9Se;7>Oere8agR)0`L5EKTx^DdyaxXafSve|s&Xmz*9^@t=pY>LS zQ-nk{LqgAkCp{l8{vDI&TfGiqlVj+)*GMlC1`Pbrifx*_#R+D-(&?#tH9u+Y5D23B zpnd_208734cnw-o7nS!P{USLUZfd0EG zH=LS23~d1=8h0@Al(|xrDU9!+I=&=#Q2OfS4*h%{Wu1e6UXXl+EYfUFm!VnKjMw_1 zxef%cqm`{kY8?XS|Ej%{&it6}GjA}m@2r$GVDvXQ5w#N=Zxt>PUmjqJGU^?A!WWI- zCm1^buNG6+DrV!%fv9RzUG6S=Mm4hKve1sVo*y&$C?P*PYs4xI@b{WrutZhO@uqVR z7bR|)&P{M{UeF9gp~x_dzV0q_31@Ydj;gr^^THj}>$%LSl}L*Pok;@or(4*77k$Ys zN$g1ye-eAw1~on0QvZxv{(>VWpQtHf!~@zkkVKLj)%QfgTKA++JIClvJSYIY_;Wk< z3Eg?EN4`flCdJEh7(#-{KoBcTZzR5F5VZQK&85Rzu5KCJ)=`N`+&+W8JK)X-Z8YJm z@LpeWP(>lrp2N>hP$3ee0pEU}Wk3 z5b2HB>Z$Vq*5OIFm;O<;6W50CiFMjEnCF?FPjYf8l*?UseY0rS<6O%NnSutZGO1bA z`z!=EQEOOQDDJ9sdKrt4l#?wcJ2QFX*cbD&sk9o$+D$i_#PEsUsk?W_ds13|>7I^% zKN3TEf?8Wcj@PEFl_!!#r)qK#FaU!SfTK{k${CoP{uoe)!StWib-56 zcVuiEB)6UG`0s6oU8viTE+clLWFf=LJ_F018&$C&W#XDmjPLbjr-pNJ>=XaEnMhrA zb20CGHNa*d?~SZVzvm^|D%_Q}DbvK_zt5%zd(LL=*VUaeWl(PJKpm9b$|bqmP>gKP zA+)jwdW7zcVvLuc%B`QY1iv8}wf;bl&f!>9Uf87z(8{6LtvnV54EGM*4vF3*qft9I ztWtvE{Av2nzcPJUm>99|YG1QO!Tv!u*XS#)Hwq78Wc08BMnfI=fI?I{%ISK&Lu5*! zVe12NGqazzkEeoA(#jEU(Oh~`GoQaVL%X5=hht>t@mc;{znMia^Q`4q2UgO~ zOCVFx2dJql3{)amco>UDH+w*=RfbY`(C_&f;}?Y=)U+Ns4$=n}dEGi}?S)q+)sY2c z7`$cXqVSBpq$Vd!bgR@AaoI1T!9}e>DNbs%EdiIjF%sA>xolR(*xDyN;XPI#bVnN)@WVn^~U1RJ4=gI`BzKuDekf+7m8j z{yA&yQV5S>ydS}B_Eb3gYLI7wlqb$a;TJ%qO+0q;#Dz*1k#?x ztd)M>3{Np^I^ikF@yP45Q=CQ~9u?g&p;I2A+F^}aT2Z@Y@mxGtRU(R?_h3G-t(rh! zRLFAG`MVWOQyh$;l5eA^;QPC3J2o;5u|a)Rd_A%BM|;9;3$SFF(aPBYBmd8&tKIHl z-dPCCj26s5-(2hm>plY-r7qjiEf2Z-*MVpCBb!n$^bmnZa;Ba4R`wEhvWODXuk8Ir zJa}oOR7+7izJzAg#634h^-R91KeStFOBgTG<7OxdF4t6mV7>UA-I9wn^t_@%7HR!Q zaoi-c{@sq>RAu(zb2|7GvM&kVtp-gP0QHxhCrP;jkiF0d)IZnY{jZV_=;AmL-OkTJ zoIE!?=~<$ym8Xr?OY3n-swxIy9!<>8T!fOYHJ;L_(@Wsa5Re zN59_Xy_s!jEAqfAgY)k<6NtR-Dj~mwYH}~tjQg7&Y`|FhUYFlIZk0W-w{9V_mxgo0 z{s_jtyK50WfXL^l#k`j^HV9D`)5{v#0@O*vKzM_>{s`n_wKp^PwN5+6{5v+J&K_rJ zzMEGVnPXW4#yv$HokqVYC^fYSA$@Nwm@m?<_s$D!UgX6&Uku?I3I0+jEFd>b<+k|_ z4IDj{0@OttS>`^d>qTD#uNASkp5)zZY6QunT&MJ7#0xHf^|IY4*THc<ha$*Lk6`QGJVj}(}gN8B$7yhfF6 z_C$m`NAM_(;Mmu3(nk`EL6LjkfE$UkWP?OvC~ts)I+UsI-lf$U%Ftx9&|Zvi^xa{# z7MBUDKBBGo(N-qg@|VmeU2erAFQ$O|7j3B@XrsmSB5&O6+L^7sufGtsS|Zhz-4M zW>NIo}lddl*})hAvN5NpQb9swH~2O6bvorsOSc zI2uuHj`zzm(z$tx@1M7BqCr&!i$~?5-$vU{-WoOPo$feSGdX}qfVCo)S#PhD3#X4Td$#v(kOC*(eehx0={|f#@R3ysG=uZYSi8@ z7 z{$IC%b|4^BjaC62)KuKaZrwsSX1^u4L78F%yChYpa?n@v+UH>f>!U;TSDjCZz?*y%@)(Tc_fUfd*49IQJxqS?F2mzHbF93{3RHV2N80=r-R<)xaq z(CkVS2q`) zPX#|i9}9yY{o5xB&mPKAh0c~xr5L)V#KK>%6&=%Vl;=pBi5MoA#PHu#bcDu7?Cq2$ zo0Ak~29){~tQH!6(;+SccMK9kVhpox<+iSWwTmD(?)dkr)6ZG2FmL#842kWmmyVRB zVbu4mB7h!Gn*15^miMdVV2S(705t6DXEl2^q#?OC9n2 zH>*-a^=sU@g^9c>KZ8U1nlXhyJX_jTp8LwL9BY}erAL8`FP58LAA=W{8rCm~Sbu~KeP5aWM9~nFhu%M6IT1QDb?3|B zPNS2FXQ!fw);1O>(AuQ2I^C(Ne3T3`KCn#N zQS~3?BWn9mbLP!e@S-osLN{uY@%A*@on`d4|F?`HgU4l6{9^QQ-x1z%M5YiQsSXkB zO_~QGmu^W(r7iLr_%2NhV6(r~7JhO98b`;5tQTrQ`sAqc)Yu*@PP|bmT!1 zM$i;=n^fLtFYr(>cW-~0WwYPTd)!gC`=uI@dK$6Dhegi!otGXh5?6V2f0&kjkOJf; z=Rra!Eg1%!!;>^^#+d$o4fdhq^Y@2hR@+5a*gi6=rXUOZWn9lpa2}(g$xcG>z)YOE zIB4(`)7M~`zY{oU^tD4jwBrJ?(=Wq(aSWtHk?|MLV+1l>3|oqb5K={}0i|fw`H{Yx zX|g$WPJe%T*0kF1y|)_p4rQvzol}>jO=UPc*g7CckdIObyCO`7*>J@H_BW5W^^EX~ z6^+xe@kw2{jAYh2CoXkF|2%x=S~4!I49b=Z&%h)CMRiLxq04+Bsv@0_BvUF=6`q|r zpOos3XtLuNHVc`v%^h1RbQ+j9^G<;0?0V-DS!1%`WnT1 z@R6}n*|+TkLXO##>9+T5SNN1awS2OAf7x(nqv{c_bT-d?&V50_2hz&)$6;2HfT~yJ4?Wp_!C2+4TelcyoqS|9e{DnOmS9^T^`HJuDyz? zxe+vsduI7|_NbJ(QbOIxr=9X@r8B7T8~G=o8{N5>EFidUzVs0HH;X5 z%s0m(*B$%yZ?;?Mi)LRfJVCE&E2Pi^KvPl9!jhc^Gsl_)r?v`^eFnx0UVH&*z@ecF7W)Bk0m=$aap! z-I23|Rn+?C`^)9UXSohhfEI|FkRCN>;LjaHK#{L5+192IRW_Ug7gQ^vUq-dn!VZ@1 zV~oOWpZ%9YP2`Puc^Jr7y@$GduyeMC*AYimm1aO1+GW}vhV9CQr(hEM6cAZ5e9|hm z%ZuGfjZH~LKfkwuoF;Th_LjIG$^)-m3D0+<$3kVyV}q{z+C8h`dnZG2f1rBOp`I?e zBE!}Gme0c-4)|_bR_$gOSNfVZ&TPXrJzzT&E_~M8%iD`|$e)9y27}xjcK=`gU8^1P z4ZOnzi~V{jo3xo)(_Si|70CA$$w7$``6nLFeynnyWrXz^m@S3=7R)$U6-~a#ElPoH z#SZGoPwJ;C!eOM1brUDv$ULHzr&{#d*|nqe_Sf>H*^!6e7dFj*60KhCtX? zb(?pt(Pj&y@#xlwB5GT4<&@&rZ-1YC%&KG{=+Nz=d9>FQ2 z))?QTVTk;y| z5uy}C5LVKWDB=BmQCJZ&DNZ%Q>JwOtmkxiX_iw~)t$0>wg!krf8Ekxwa07zPf(*81 z&XPNFY9|xpfo4!6WwtUh&HG=~V3V_mUq{)yu|+$9%Yv|Q=EqO-*TOcIuzy&~E$O2< z*+MdEn^Ai!oiWwFdq$C#tfigi1vwiPjzjS4>7#}yCrd~FShaTK9Z&N8u!yCaor|DZ z7nf>hs!>K!^uIL~pG0Waq23j-iQY|US55IQ{Or;r$zi8c%w?HD=@#7&4S)6mW~qJL zzXa4HB&b=D9yrc^02S}_40u7CGBa_G@*&mRAKup3C25wXph!xnc=yjxyUC{Dq{hVi z!g+b=)Ewxhd`9TRX~Mzp)jPDUls{AB0m3rsj!Vr>Z`tzMs>@navlvQJ5c~jbtiIA} zn@x=3xrDe#`5l0Rviv;;OO*Ex=`!GMCV(o_QB`qWC=se9DB2?F{PD&Dj$Cnwn#yQN zeE#sKOKqDdq?yQg$NIv^&l=yxF#OdFxxPDE-+=v0$XO@;3S8-VGwdDb@W|ztkRRvx zg!ziHizvkG0MsFXr~#?k&9jOTFZrqc)3Dd^&+S0H$L4LCevf(7jametAHB#;hQY|? zq}ym=v$Q8DQ+1bbmX4%3Ro|7T?&Y6R;*Hd#4li8e622!1D-by}+iYuU<~_*Km*ZJl z58XaN)lc-9{CSx{8i%&`NJw5h(sC*6_eJ(e0IxkeMX;aaDH&@9>+jo1xR3@5l>z!^ z?fs7{m!zFe`*D_^NSqh^3pDrdHvn6}9k2co6^e@)eVu<(Z?dtVa~o4+J!gU|ZvW9P zuUsJ}Y<_ZsUPM^{5)v8v72(T*e{p=ABQ}cKr&ri`V-J3YP_Ua#99JV=WU9?JIN!g3 z7zQ&<$;G$Yv4;#E?RIOK2Yyh_ zty{M~W-d26qDo6QWyNIf)6cDHLD5>Fy+Ry~^2;X%%j=`@kCvLu1XKpo_F+3?sBsiy zz^Y--Z?&SHHl=y;KXy??dBa^}|Fw(@z1H?KpCljGUiHI$7073EhCV_gU~$UKYp}0* z54DIdKTrb4&IMF7zre!(q;?YQ@D^pMaxz#`iwfIPO6%-r8Ubm5&~Z$u4~X%aY%<)N zdJ3=-OJgRvk=y9)@R$eXd-V~G5o@!SJAr*ZlP(kU1BcJx--KiX?l|~1I9PSyye6c= z!GC+3+9|Qzji|N9{yFqFqDLL2gf85F0*_?8jbHmy(?{L4Rn6k4%yZx*`RK8%M<{5C)8%b8v)ak zuAX~r7VIZ@4Q}ZmGEe8t_r24K|IUj$eT#uU2w&B4{JBOVdReyZi zOLNsZK;`bt=2_Q(R77n7W8dmYYPKvxUx``#AGP~Z?z;;gbgt!=ybG66V#fEjLh4M2 zL3iP5e2aqr{8QTY^kDTmGsCpf4LKMv?zneLjG8Stg$vjJ@-8kenaU5-Qp?z?Yu*@9 zsScxC6j6a{he8O)(3~v4@2kLmv7bj!XpG-j956h2{@On?b)5f-rHClpQ+r|mh%dM} zVs(<^Fd8Sy%V@nAg0#pF@BeiPIIm*{7zeu%~|8vCl-$4aDc`Gq&na8Y%;Y7Ib77-XF7 zooXg+kU^|Z^RdU?g&dvZ!MBPy8-}f~t^J=N3x+Oi(5**))$_B?_~DV)U_k9n=6#uc z=WtmzsKI}MTm7{r*HC(4CxUq&ZN!7@yX?rok4KOCA6;5 z;Wc0C1nrcIySq^=EAxYv>DYHm(%}|pSp!3#SBD>KcmgID04w7Yk)Q3&-Ug@`p`5?t z6N9!xgPbBY&g&gcp(A9ChQQxF%#ZRQq_luywD#YXtz(g$@}||1HEOCWB-Rby_6`{o zs@AcYy(?PbVcE0?UDG~9bOixK*P3%t9B41Nud?g`Q>Lw)Cd>|G9NWtBn0|IA3|;Z+ zWj~L)*~QM!yZO&sHe=N$XR5HmJ_~u%0pFVYlpOxL3jOg22;P^otaQzzqJrE+Usj|7 z=p~^XXWcs6EbI8RYPk{dd}hnpg>H?AQ&M+4@~@e;x>T}ZOxCRf{;;>H0H2J6e%X9Y$@LyI($iEM{M32JSljv2sJiq_l)(vlIT(_Nth>8zfqcwl}R`A$S zPC2KnBP*@ai9M-I5<$4k-c}YQ^7oNY@$J_E!u$Pt&*IJ@`Y-Sm%Dctqo{m2|2kTws z8T%XIP^g?f7DSmbZBiPht$v)>rG>M4j(v%aiqa3IkdGZ9UmU@e)++peew%x$`nou# zD9(IzY-!XDnL2#pglUUQ<`qdtDPSy>vUrALjr&Ah^{x8&j~$n*2`;mAS-Nbd6%~c7 z9f+H8E(CpAuiO09IZ)I_{Q6Ez%^{*hM9vJgcBZzEPsE8&B~DCSKjLeH7K+KZ900DX zk5BoTnkvj+QkIIsWjvFX8P)f!^G^MA>u8^CF7A4BBLs|2^I+^cl^6G`_^fXEo$a$NKi~C*T_U*gNbul#9}tPNdq* zI`t1rth?8Ti!uYT%WzIV4fxU5;7$l$cpc4AxYgIn;CTB<*yqPn_mF{cP5`=|#6*Gh7Hj54G%5ZA$+j^F>(#oX z08{px{zjNe$h7OVYFNt#O9?N2eS3`D9;LdlY+=_5a9Bgi|8 zOLv+eA(Npo0EeKB^}QwrQ-|3Ca5tfTTOt_~8qL4th38SSQg#Sv)VjbY+2T;NXRL;> zPrP4h9MZgSOs{eA;i}d`+JPt+mN`GjR?nq8Jr#H&7Y2elhU3>FEgf#178RhZJUkx} zvaV+HPew(nXwSfA!gWr$s{Q)4xk!BErc~$tpHdx4Us6#=gL-R95BqV;9`#FzkNvl>K?m^Q{s;AWTb}4OyxhO6I zZOL{+yfO&v2@*-mXiQfD%M(6T55x5q&@9giG5_tz^gi4JVm36nwsqA;R!hR^aVyIS zNN^}3j7w8iHK@cPgw{8!yAt|?$ z1I`UhjgdXuk((V=+{{J{TyfGNr1tOcNRDEe$0N{O4@+lp5N#u+IeeIJ1<`HY=iaf3 z*a>`&Ng(Ve2h?%`1f;;)88K_D!yShVnc)*Zt3${Uu_OA_OxtA3Ep}ox-NeA%0EioI zd|rRT4o)Hp)D7KOuFlTV*<2E5NZ&KN31xAbSrw<24UA}FzoU(A`nTBKnzHG1C3%)# zF)k@BAwe1*dFXd-sJ|8y<(1a!;1_fP@Uo!b`YY1b(i!XA?;5N1tI2G;LkmfRP*GeF z^ZrT$BTBd#R$be=A{jeUKK2=q8g|&7oK3Q5_-%7hQBrx6^-jxXK3!aQb~ZV@jj#Uc z^?|xKwJN(;Vi!hjJ!+E+2tjasOJeJgetY`e<~zhr%jqcD!;pVC&H5*tv|w2^WK_F$ zRR%NJv+I%pS{YLEE1md^%KadAI^_aqZX~m*Hb{~kfYx&FNOlf-TCr@)(7Lrbx4YR{ zUnC&mH`Af(=2fP4{K=fHfWCHkdP~`4Vo!RDKcpPPBc6;pKCiEqj_rTa;+O~F970VW zpJqB(2-T_osS>)7dLZ3obl{u>0IeI@_eN%ioh;)Jl4;p2HDs$rofLh01IDTJ{0VM* zjXMGaqASt|tAtMJ^||KyHd$Sk{gV4b;EoZX=9`!uXstVxee+}^Wry!g!B>r+x9q*0 z)p9KG5D?CVzGW!Yb^mhG1T`Cojsqcgwk+4!{qu;QMDThRAWRwM$U$3Q^Ga&CM_QMixsYCX zIWgVak|NHf!cu53TD^Q>#Q0ZgG8jIrA}B$r)Z_Te0`Q?Ar*jX44~h4_`tW#I#tg?A z%{9b)jRjw$h;i%xEI&s(7{GHbU6h7ZK{WG)3P;OMZI0TM5E7$=ihjXe9m^f9Whhma zU5e}?xa-2&i1JT?fT(jkDmL9VB@w#VE2oc>x0XFZ+$M3unOG zwhsAHHNFF!tEr9ErYe{CiziPS5K2TF0E(f|c@wp)-?;<-X?DG$)iZM`d^FxjR9InZ zim)k3fo*OADZ#~|-?nV;ZO{rkun_zFvDr-$idhqf~Rj1+!tXR|9g}(4U?)@h7 z1I21rS9I;a)kcRBEVE+E7=Jp0Xfz;=`W(in@dVX_Y{ZaY?eMP-rb&QPAUsAkA)*R` zo$yBwo=#5NX%QCimU$paY3iA!;~I@TZk*Jw+`xxel7dy1Bl<0}8`#lBdW(gpeQ-nb zkpVH71V18L_m4DO5Pg#OY(tfLlmM+gmDt3o%V;y(=P&zT1a2bFera~cPQ zZK%<<33dzx?39DD=0i}v=OdUYn36;=buUK^-4&aBKHfg?7gTX9DJmZoT(7jMOWNg< zwqDq1CK{o{lQ@T4i?sOf6@GJx4p|b8Cp>gt^ffr`KLvQ`VzUaG9VM5u1GfZ|LyKJ| z$j#b8M!#yGnplwng7-e{q>Mj_v#!yFt8{M0J2tc2|Sx4+MD)~=c@CY1V_)~YF zD6pW|mc4k7U?dHI-y5=C_>r=j$?n+;e3#5E4Fb~2FsypS?L3;e$LdV`m?PGwU@*Yt zz`L$b@kPV6wA=1w`xQiEmm7-X{Fa)sSl%ij}J`1?uS^3D8A!3z)b3x3LYpg`*-uFvrxIbkZ@apN)!++!ba_7!!) zn%HrE$XM%xFYg;dcHdP_?wJ+hvm&YoFG}xi=#xow`HC7&I6puIMpRZPepadzJQj+p zISDos^2hij%5qHbOK+?4)*s|4i>a>FZ~4S>Lm4Vk5s!IeXS_r4>znK69oW- zw~@nsso}yn{=gmD4tNKj728&v4VurW5%dTHtb+^QFU)NdK8(xg#*pkn?i8G~t-piV zF3Ps3+Iz;txNhRedf7M7DgI zo+%6+4re$1&?_&cgss;o#1}`<{wqgXEomDhv?kVdlZR^r}>6@n6 zKn6$Yh5D|~{hQ=E+IXY0QX!T=k1qO=`stsRi#ddL z1^c9yqRwrJkRp}lkw(Rbxa_>37F@>k`PDkn=crSC$?~4aJAUXV_t{vB!lelzbjx4pt>>Tx)fr`E@Wy*u{x?7@~nGZNQC z^g1&@>I}>cd(T+4=jicY(E29c)QWR$N-;~46K1Ue0sQOtMcjm^EreM;vFJ@~fP zwA8~_G)RF1U0*SNtKz%&WrNz`@BPP&wBg}JB)I(D4|SvTX|Sg-yb8%1(Xn|BcxGYmi0O2-9}XSgNXTK>4DyXBrS9&P-Gtq%kR- z)c2Rwd4I8RKn`6 z4cL6>cLb`-9QjSQ>-#Q~Kw7<}U8;DuDZaKPE=l5Hh3yGqL+v5aV&u@fbqFx5lFzox- zrSr_``JTQdY50vBLXPZZupu&RnE&}~;_3kXJ8?r^nUBNq#{dhwL^F|2sh8S_?Y zdy&H&tEA1EZH?mr>#LzZt1Yohup|2xPKQqz#=#?+Vtmi~h1y!fUd^`p>Q{e8$IeV& z>qk6Lluc9AC_R!0-Q<&VNKjq(3j0GFul6P}Ur6=k()s&q)e9NDD#O7cR9xbMXhXTV zr|1{bSIVuD(TR!02YDoWSmO`gdm6dY;|_YabVm7kWHmi!XYp6i)gln$Hs9+#X%3Th6Go-}S(;iZnUNp^k=M zu)LESX>_1!O5~NZeuy2qUTW%HG?<8)G=xW#z4zCye^?iM_oPDP*x9Qqg+QG7&y0@89Esm{;%8@ftNMEXT5qtPd~&X{B9V zXU4adU$B`A*kydSTsgv^SKacM`y?AF8C3KKVApNe+duF~Ut%*v)4h9D%&=WpbA5$9 zQ5W0bU+HJdlP@50WvZNY=DDAtYSsC*4G;$HnF2I#Q3=-4pn=@Y8(aL1h3u(?{r>DV zN|+_hvIyKEQY92)P}X+(#oBn0L9eB{{ZIFm7gch!w~U&kUCH4LAA!?W&5dE}^^Jue|2 zeIh-_AX98AssOZKq;1-;D5Giz!OnZoe1D$cMB7VQIK$G4zUnde5`_;&)g?1aVby`n zQyvIB0W-)=G1yMP?Qm@{5RNjy5r42cyQ>TNeDY*x1XZY{+qac%n?>H-UuSNDEm!P6 z2)gU*CpVbFG@YHW>SQBs%(d}~KjuYi9GODlpt0#2b znE|yHj_u%;&Rbn2vQ*5@)KQ`q`{4bpP{Y;s5X(9Y{_{ZwBl7!}tmziJFTMrh_UxfX zs@Kl{t#y&x*nh~BWi$=bP1&pK8)yQSx(v?Cpzx;C!#uH*h6+2Kvb)uYofM}H*QSSxO@uEe_Ae!cchQv={Efm z;?n6IxnV}rwE~6jdzEbOrjTLT{hM)V?O%RZDy3NVHE;5h0hQ5@Bz*`qL>}(_h`U~W^(x8WVj5Ji)LNDzHcq< zSgNc8;A=q3wXGxW<%h>MQg8WJ)@I$G*Pe?H*V)SkBEM9pm^n2cAo-9SbE-lh&>y4KVKpNJ5<}7R4H3Z?vLg$W_ z0F@jT2V2oA_w01sC{w|%xnI&AN0tZ(?W%2Cv?>kg2Txz%e^Kf%_K!?N$ous2z@}U^ zR*<`XcmF;?;v%YN=l3-h4cO8m0TISvlZlb|y)NTjUi8`FXMDnjC61<+m0KLRf>$qq z(6efQLm8_w=wGtN3YA#rzmn9^WytETJ%L|Y`I|P5#@-$MAMHw|4T^c2KL3_k(FGL8 zL;$&^PM|Vn5>Sm;M=g>K) z#NN9@Uw`d5u*u;L$%&r}bG1}Vy^WQPRE)!McWpV_X&P}1|)@B3+lRFBSXH zx3PLB+GTqkd*fq-?+2h30_`N6ZNE}KvSB+1`PS4o;D4BSy9+x_LJ-3gzEsG3sxz&C z8@CQAJgG?AnGVbhdyk*uTo98^+BhXJ+F^hyCE*nHzn|{V{pSfoT&7{i2DbAhN$5|i zb7ERS5wA!td^g*HSk%0-Rp7s9@<^evY?Kl<%KUuFr`1sU{;niq9k(}UyY_o9UBQuK z(hr*jAGuDjL)8Xt3<)8d^B?Xomq6+fTP;xvzNY>ie{po~B}##t$&vfkO3%M0*F3 z(qUG;*_HLOqL+d0TS~t=hG}sS-DKhIz(Rs#RKi!-sQ5lfWSHnb%(|a%!0n7@u3$-@ zw>Gb_QeJLhFFv)Ii46Xs#&OJ=Mo2EcO4qQ@rUf#P)%3hZqr_6?ZTADHZt~ZCNim@8 zgbB)CH(R5?XzolcXKyz~ouG0ICGkR6UGH8w?aQ3Z>Z+grpuAMD!*$s+3gP*lf|EkJ# z(j2jmwO{W4vejo8f6`!B2RT}kXrQZq!##d&?X0j$2jOH@NMDX&FQq8PC1(Q%MRsc= zCJijcZmbr4l*{z8a;0)#C}(58`WAD{Dh$C~$nYXijpieR?t; z>$9aE8+hqfGj8ece~7DQEBG@-G3JUiK3Aaj(vc%uC#R~#zFv$~3#|<_!Jx#&4np3i zLMBCqPekwZT)AGJT`Qv7a5ZY^OQXYe1Mzp}!*^9b1#5OsHx*15zDPJ4G-J=wwHQ%@ z*%N}7k|&{(Z~QR>=>Z1}jQrRPT4sdr=*_!Y$%$FFX0a^&cfltAym$h4518xRX*jH^ zw{rfwOF&dK?M>Sq4X>xxt$rz9+k`Ny_OVc>e;Rr;RReE2vgvFz#` zGS6BX`#Yj&7tF=A*84~R&o=HMknA1&W~A2IVwUy1BAN#yUR5puU(@jX_C)x$FflJB z^FR2j;ua?4-4$AagG$@Yn}4R)d(&=IR0oo)B~+Vbb@Zp>u1#6khuiRh5A3}w1#BT7&>aPh$XW^Wv-fTuxt~vOHJsf*oVuk37W5Te&&P6{(UK z%c1Nbz}}2yaR=V(P#;%BNy|hxr(d_*cjwMTUCxC$Wvk=&kI9uB3)E67{MTiIPLeY20N_0vhdabneKOA!;B>Ur!LZGAd?q~%rr~)Sc>89P~KqU zCnC$=MHBC-S&E5^&8lm9cWL>}ZYC|Rd3i~I(17)#`MNBC54m*odT1a%B`{>y0|z@_ z(CGTw9Y?;0+fWEq9XIpl$pAE(T&nw{5qm;B7F@{o5Q;VLQ zt~9aiMqT-=zab8xbcskt(x*RDB)j@+p5V}BlS!pp+G>9tBfY&D0~D!+&8V4=hb6ZTT^1cjq@^4xrMTF#Q_E`zI92lR~N5v->XgbpWHHR z%2!|W-s+r=3=OYJEf_5+fM}d%x`hj>&3R2V?0`uq`ydhqCP1~hb-Ty4-c9xIjNhyw z6WpKK1OIbkq`s6;z7%jQ;mxr?uT}5CzO-14X{DCcS%aD7_Q$;E95@bhcKH-^w9s1H zsfs5St*5cRg)5OVx&%loOhUwS<^IEeVv}1@GE;dnp zvlxVZSAv!@3o>h~7edN1vrF4gr6&V0sIy@xK0^s5N9z%qFAOV*$QRCfJD7tb)s7H| z7NYdI&4la6f>6)i$z5^15`et=Mxdj9SPK7xt26dPX2^1E-X~dYXGV z;kkQ_G_R02lJm2V)ege$wwK7XED#KMQSwPm6V(Fcv~@WF_)9UfH_tIp*ayPC8q)j$ zB}+4>q>SSmQQ^1<%UPSG7}00%)+NI5TRpV$g^&V?nI{ZhfL#SctTQ)6#uxi_vzhSl z;*rzJ|JQbAWgg%+!gnY~F3r5WEKKio^=gVsy%Ycix7_}@Ps%4lK~1^@NQ|%2gk+*z zx#y5-p3zIG%MnD5s3Spz63Bq8b>9v}bq8i4(|fZPF9`re06rj)^u<6}G|5c;@$O-{ zrRK%w_R_)}GyoWj&nUOfC_B-QTamIKp8u-`?C1SnvhPz*%Odr+lXgM<&-1D-d+)pO za|=*XafJ72vF3*9#o}UmS$+WR%vPt<%z5YV1MH~*)YBj6qf+TX32jcLDxY+Ry=Stw z0AMzwOa$;M=uY;L7`RS$&f~Rjmr5(0u1=dqVRO++CR`fBVBz~ljpW6ih&|KfnhIzG z44l0q3=WGXC1b4n)bnhI_oPRD7-~9|_WPHNJ!*3n>TN@PO0`;0E9HV%;^H>Sdrt{# zf*l`=;!XEq@A}Y?X$b`zvLx9 z^LsiIk)PJYp@hu(XCLMMY#UAueud0TAhH$WErU8@buQEyH$SZ^f{-yGG^e`HDxjw1L5P>Lntam0~+JY(1ZUw`=wN*o*)4 z)IPALv})A&_Qzf#mo5%3F~EMpP@qdDX)u|219^argp2sDdvDk>UJmL#8L!l0*DT zZTQ?P+Rx<=mUP#fm-X6NKNnEw&b@n3!MJcibhFGHZ)>AZk$zGGfpR*xWlZD|EIs^@QL4Tn0h(XeZmoPM*ywCRLR{RVG$s9ghy{h|NkhNc1M*fR zBWr+dSQvb3ZgPWZJ41+%mz-)v%PvXauPz|{0}SoK6#I>te39|pJq3g>&eE%RY%6g9 z4*@dvAlP7I|7KXnIhKN&0VZaW9hwl00le$EO?~FFFp_mYXE2o?;f6i)d<+H2>?%Vz zdkrp*okaL6K@im73`-sNA~7;Qj7L3@QAwas5TR{oStlzPW&Yw9v>CbVTK2_N4+hEH zA5KGZMxTcGdVNwLs4v3#IiiKQgXC%0tO4D2$uL7m0D+rS#?4;jHV4`wM4o)8W6RtS z(W+ z?~{buL=G3re{P^Zx)X%Kz`3qq(0duD`F7oeNb3Lk>UDSvQuoECPFVp-MH;uu0PwMQ z;s$L77zvHni z(l1v0Cm#Q)ckaD(6QtxpD#^v^N(?=Uu2R9Q>*IejMzB!J$a+a2pQ3Oj$`^$iLz&z# z@#!HFE*+^o`BH~Y{v3|Ss5=T!w9n@WoOsNU1WD|?yDy#@M~JuFV_{Y}@2oz?Hu{wD zNV|<3{L=Z8fFv&xZj)W-&~19HZ8AETzS4Yim)^B{di>B?CLi%FiSxW}mZUQ?2=y9L zYZTAy%6)VS9O-$+21UoPl&Pu&HG-|Exw51#7cDb$sCKCN*VmieTt#NN&7A3w27-@I z>id*gKA~SGi;e850&{d@h_4*#}; zfBV7zn{mLHi#A1qr`{J{esNUT8Q6c4;o;H>^X4N?JDqeLJ1G&jJuG}qMUlXGd)5V( z3(lz*TP*W9qTK>du`j$tMH!;Jya{Ni2O+K+{YuM>Vf>UV1?Oc^Z{@&r`-q2k_Vw30 zU2+2Pj^H;cO8k-Us%_j-v#8s?EwAO3zfF~8>gqrMWx|*BKN256E@}*ZnL7P2b)4#;x!`QIt2WpipE;2gVJ^TM#Aa4c;%*5ia+mLBs2^)NjZH?eTj#^!EV|LWfRn~E0g#^ z9d=eJQK!wA4HUnl*ySQQ1`__OSs{ZZvxSyOtH`pNtu=CqWaD}etU8D3`R=3L(XSO6 z5uLeB6mnQc&t|PYoOxZdqfxxIdjFAgzf+U-@4kGz0e6^PqWm%@nc+$PQ72NT@6xD7 zZa?rPsbbJ8!J}vMRhn>Wcmq35+8xOJL&>(=PhMO|5Hb$=b&>L$K>_e)&lD;51gC6k zK&4z(;69>oruwCadr^L3|CT&4lJyPlb0URL98|p9jlY~s55^w#6hVD*|tl8-HH<#kxd5GLsP}r>rE~paB~0;lE>Q|7~d7CX#W_@4!Da@ zZjJbLemL^}PYss9T!d*Ll*5b8YXko&#@2@6o2tj|9~n~X*mlg_$tiO3!dJV5-b)2? zgHp#^$A+q~2TK?^&|8RVSu;%0X+2C4RU&ZWn+GFNS#|+6)n(5C`)Z>A!zJ;NulofL z%X^0sO>~k(Q)S>vucSO7vtKe zAe&RfcSwu^9~CGbSyom>gAW`+R0cbr8H^4F6|%cyBdkg^&W?l&T(M$G4GeT8W!s4_ zN@(r&t|%3AgfbkEW1tJO3-LQM8y;rw()%a*iMb8Scc27!Q`bBUkAXiWf-O-Tz{NJd zg~yzExj#l{<~!6Kw4z%d2quZ2J?p+pXSx5@E}sf-#~E8oOp9WhBQ!__YvmZBTqnG4 z_i8&Ke0#?8Eq}wZWavp_3KCgU5=IJ;%r3qMDrBaKyHUJ=w81WS$g9RuRoO-g;jTNp zibG!-#yLaEI`bs_6&G2?(5o9rVB4}(fe2m^(-7_|nJg9AqG%l8TmhsI!Q~8gOLBB) z6RAZ1T)n0%H`(rO(Yw&L%J$(cqu57i*-NvM$>>Wfd^@Ee=_&I+*q11eO`tfnci-dm zvh8YCB2U=#2MS?@&ULgO#ZECF5^P6^t(dYPOaM;LFg8)rP|c$>$i#5y5*$I9u}sLc zkWX1G?TM3$k!SGoK9q|YWAF+lp|cA~oWGhWNlGoy{$R&%?X50(vYa>>vIwbZsAsC1 JukCR2zW^bl!WIAk literal 21111 zcmV)GK)%0;P)A1Dsoto8Hq)#8FteD@a+Hn{QvCe00030`1k($`Tz3n{rLC) z_VfSt_5bwo{_5xX_4M}g@dyY9^YHKg^6DNJ7Ww4j6?D=K3=I4C_B}j2{`B+N*46y% z>ig*CH!?C86cjKoFDoi4M@2=VpPrqXnM_JbH7+a@3=7>|U4(#t@a*ei!tUzn zY-wm$Q&JKU5zfoX?C9s#JUkZ=57R9z{qF4~BqYhj#b#e!Ut3z`;^C&FqGn`dFDNJK z=H($EAk{cHs;8!KZEfD#+9)9)iH3&Z-Q74gHr&|P(ap`v$j8@0L9VN+nUb5cy@LD@bB46OV~z692^`O7#I<9(c*PTKqx0C z=7fU}anbLuuEM{*?WCi>yt|Bvi14+wvaha&gM%X*8n&{r+*emTI5*NMDd&ocxwW;| z)6+;mKhYy2@w&P0s;OaMU)xbp)HF2en3&&VWAeYg>5-B7;NRqae*g0Dk&lk^!@~8< z%i(Wt_}tr%jEjDFck|)hPDe$PMLghXX)}b?m6DNOR#ocR*4fF$@7~+v($3>~dHMJE zWS!dd$Hup*ruWsH|MlxzncC9Az6)>BdvI;kF)@)rIl|84ab{$o zT2hdNfs>An!P@l3wzD64)5^QJppuV7kJ;+X$BBJ=+UfXdrsBP>tEZZl`TG3cxw9#M z)U2PJmWqeZ8yrxU+`){7sb^rhe|Ueh=EAbCvZSG;zToEZ`m4_Gda2!%yyn=~*0OeQ z(beU+$l{5x-Hkjn(4?LTZqN&D&yBc6xBvhEDRfd!Qvd}B_rm)3{=5#x@$t>n<{8N> z)#Sm~vc_y+p?yhBJuPy+!~4PYg*!K~#9!?9|O_!ax*;;k1n~29rij zrnZF=M7tOe7u~pXnQL|Dy7c1u&5snJg>DpW-v@Fsb24*s5<)Tn000000000000000 z00000{{9g?c(7l!DI z`&1dziun-}(^{G)i86}eQfy9z)W%d4$E`PRb2aA|q&=djGO=VTYb16Y&qt5(qeF9P zrrUL0HLYf|S=V)nt*34(-Mmt|YNhfCKU2^^l(`Q+-W$zQO0U&6Hoo}uaAvWb)Whto zYCF0c?D~ycT`L!k^KvnKanAj2V{%mM8*bBYcl*Ihot?+4-Rcu?xKOKgzkjH+Q>*7& zhs(DfJ!PjfoUi#AT!~e`yv}O2L|SahoF*rXMguvL9UrP=6AHn3vIUjhbSQ)d2>MXfJKZBRLglTI4LNB7c@NdJX*M*( zs0&qeX*M*5^a1adPK2u=RjHDt@d@x==zD~#pcJ3&e#`8>jt`zT^a0DG5Uzom?ek)p zF3P0KVzr+ub%9HTCmIg+scF7mh)o>nGEuzI*>*KQnZm#WBw81*zT8n?uagydC zH=O^yP+Q-?NWKB@g*b& z#SPx!eBOi;n|=C(#653RPd*kg|;|K#VZHYr^du?yM zcjQ4;2C)L;+3iKEz3o7u7|Kvk3VSI_dldAdn-0D%=%Rb*rC2gTyIKgMn06~%)kKyY z*>Y+LC$3$)A$_Dhb@m}gNw<})U7^_Qcge1_GkMIHfByMrB+r%QUxdXV2)}*{USv8G z-i^u$b7lFPzXF+%zZ;eb?|_Io!c6!+kO`}ynebM$5gCM+z=W{!gtsFTf^dyYh(6~K zh{%NN4G*Ruo{CJkX269WJ3Be?$bVW2Q16)NGeq8_YCkQUd{f*7b zx4Kd$@bx+mJVf7Xf1oa78TxmrGf8yu{LOfMgkPT!isP@sc-gcpvmA$DCcyhYZfvX{ z@t5pge7L@Dt8jfyNH!F;D*^}9P}M;aIEY1+RV18EgRo&D zY%&q1nt5RPXv0wY!!i^9Fw>8dNj;e*&|b#5lWbdzWu1Q^ItFeYxGh8;>; z=<<02tY?WjKpae_n-XohV>!g~aRNDT<1OF`UZToM37?yQ`k0oC1cFJugylX5!Uf%b z#6&oXPuY5;Lf9tkIws@~Ph-0~!rV*%iLOQl&WNS=i6WDIr$K5!wXM;A)MPR_K7x>A zFCc#tk^_~toLGX)A}zQ9riC*G^pmNp6`CjYAvrf8-6gooIq$VGZiQ^ZSFQ;B)FPE1 zZmhF)2bzTmt2?`~)9+@OC%iVvgk(QMTXkqhzZR*rmYQYfI!kjM27|u&)uYNNt46g2L!WJO|FU9H+xo?kfyn?^*pYuEW21tUfa}%JRB_+!>p{&t@qf}^ct_WX4 z5LkuqO<1koi#^yA7UyUJG7B_g+M{oNL3=}@SjfIs=?V{>zx=Qut7@juEhBfdj8;xj zmCBZp@iBqhBJ>!!mkomV9TRf6C_)B!5i!XgO+7OK84786!(Y!xB?E!*@FK!Z)_rgl z!Zsmqi}3T}Tup%K7SWu4^0uVzA1fa;>=qWKwIr~WxUCYLkug!%+-04WlZd1!6c^Ng zn*h^mOeM}+m0&VNwh2XCc@LKLI`WP7<0f?M#f94`i%P{d2SNj~WO3aUA#4?bn~*;^ zi^U#0hvDa!CxGNoqsQzi7R^gWxwbSEA&8$y+FH^M@ij@xT87cD5Us)415{xSv<;(O z!`R0JzDaV17ZchV=>bMu)>a0EQmM3ZBEtVQ0k#Qk0vEnXGa;soOaw0?1OqY^Hf$Bb zG~w^<#}8s>_Q}`z#RM-=SmOb#S7`Q2u1N|y1Y(mGJIJ;~#aSIOIwj%+>n_HHIz$N8 zvzm_y<<^)9D(R&lGeOlzVTBS{lOa>cDXvtnD8AdB(Q_8vo+zyWD`;~s+lk~u%j`u zN@NfRCdMlXJL!|9tO&h|5jlatRB`1iuX1ov6ZC?1yUvUJhdmW*{rQ*x_=lTrP!G&a zyIlQ<2fj>YLhL~-cDb04n#t?{5^EYYAiqK@JOs%WY4V_twN}EJ9O$6zz_&}J$HRuE zwMHwxi<$u9yy(tDO%2z?Ihf$7z*4>Yf%Aj~PlPZ{h~0baB#iSmfosTgDDtw_OfoH+ zlB~lXmfOg_!B~-96wNL>bTzHXUBZO;^Yc&+EBwXtgiM$wz&j7ugR8(!DYznpYQk>S zsU}?9g!oKm2LR2IR4iLq8E|%yP$BJj0hjD{w^~$F&QV)+v^Bv5zDbJDuN}ro)G#*Y zv3BrO*zi;cEt)SfN8*#f5}pf+P+1&pGBN z#00ONfS#`>Ks0(R+0ry?KJ^3!{MuCk1rdQ|8&7x;z=UnK_xP_>B=ckMLF`^tn3V~P zg$tOF{7l1U5!X1txRsS^9Zmbp9-Bl@qaE%X1pM;^{~9A|5Xq@Anx;Pbjj|a3uL*`i zvZ2-({mVi@MA&@7!vH1-+q<#dZ6`J`6Yj2#s?CC3Qh2;LXA@9EmO6^s<=mYERT*l^ zF|@Du6{#hTp0ZytHC#d7+97j+eFu(RWD8B?c~Zcd?qh=AzC&H36(+?@RNw-LaC9fo zUZj&3RP%zcdT_?(gY$g{r`{af69SkJJYxh<&*3aq4Cve#8HsHfiSBhy(oDAp&Zyg) z1n2bx&T?Y{Ox{XD-@V8jbq+fqg(`J-CEAg3Nd$HqI+>5eehm;gS>Nf_tSzyTGoh6`RY;H>+S5TaYg zO2JlTU&0mr7D5TH#t7^Ctt}Hie?Eajko%;E-SY{shgojH`bh9KOzo2@h;Nw4*97j^ zKA&(4!X$4i#{n=S6_S^4dE?Qa8-E^^gP&PCV~d5eGZq2vRr6yexT(V8d`;lksA@g* zENjrbcnfUaQ zar(H*4Dik@IS7xL9oqrw34zxR05ue>T5#;9Ke{n0l&YmlPW?bWaRR$lqLP}+sY_YK zHQ_eNjhO)Xp-OPBRFsI^NV_j}SQEKaQY2FBKzoI-gz0|L&jf&2OrZ*UmP!RLZp=c3 z&7;fqoqGBwOBQd2akg0ycD~!&ySuaPt|r;xMZtUH;PCJ;KxHY=zJo_uAzB%D@G?>P zXT^qYjuJw&rj>TPGMboDA`j~_QJ{Uif!OK5molrm-(5QuyC^CWRyYlW${$L?+Cc^shmthy2cmAgBo* z3y}%)<=bz9eQ)!(;HqSd5&zHfZ#x0L)&Jm!W#B_RnjNkn%YOt&6xhnakN8ViPl%Yv z$A{Af1)=&W@FgDQ30INj->Oqah3(&$fiJ0OcDRzjX@Bg?H)#-=aJ~59K$xbK;D`{` z+98@9UJ5?1ZVQ+6;NUml5>=b873p89+j&6{E@Xj_-#+-`BM_NzErB0@vr|3T7V?u| z(V)8X>!%QmxFWmK5f^3|{PxQqcfXs&-9LW$LA2KlQB1hLz~$v{C$qdfam>WXgowz5 zh%vY6DSn7{_j8u5413q#%|$e36uaz043m$ zg;(yN1T-%pM89GnDa&~Y)o+`_>M)=r$0ba6)&x2*DM`n(Lv$EeI9d~GzF89QFtmJ7 zLcKLX31NJ(%hw?}3@>*mAv+i#+IT9hoEmKK@75)7Y%t=hO-sf z^-ADQ$+W1|CQw`|nO3EQ;w79Me(amwY8pWlfD5JMS%w1xSww1(3dO1yHORJUDXEMk zX_~TaP5-uSitfcyXsB#$?ayXyGApjeDE=gfG&d@LIWIU*u1^>?jOUu62}4fUgo{HH&Nt2z`}Nw;g!9db znJ}yg=bT}mFl4~{1Pp*t4*)<%1pwKV0W|?btX7Kvun<<&M(n7cM!9BN6$Z0o1W!Vb z$FtQousCTMK6EtFvgpUio(yD7fTm7p$1;eL0R)*aIjRXNl|4ff2FC>K7Y!-p^EG9v zx8XP@M4D^1{ib9Pcae}LMj2rYWrL=BCPV~p$ve9Hj=f$U@?cb851a^%dV2#8dV}C) z*iDH+Gy$u$8o|6aR7o+$O;}T{PB_i)tz$KAS>b{MNe7PvU(GLY6T-40lTHC6Aweuzk&K2HN2=5cD~d(J4NH+N zT4eI5M!bY$MJg$ZM078*6KD8j31hq$9mwOod>a=f-f@-T5PJEW z0`z%eMxrL{qlFD08gkXc?Q9hqOXNLCbOV(B8gKn=?xM9OP6DhmBgI0H4|QQZyh>1h|d35^4M zeE$s)2Fy+P=-|r(eo&u4a4)n02Wvv4tkXnmq}3W}7sPa>R**<#WTcg&idNC7n%idN zGObFUsw`h29kte~C1oR3Yc+_{u5EXWt(2LPh+ebPF;ia?E}#A`(y=rT2xjZgAh>WB z z50Fj*09Iw%+U*l^-G(0k%Onpr65$XNPQNCMMT6Xgr73uIo%IN=i4_dDLN5U?&wbgP zehCPEL|*_xul_Xw!M0`OfRUjj_vegJU?w!7N~JacL~^4Lsdf_)jC|Y#1g1>ey}g6i zneJ{eL&_4#+h}b#%!JeL6K+NyVrD|%7JLs)0=O3H2)p0hM)3K{;>4Y=uK;%DCIO-G zJ|>_@!;tc-nwP2Q!+)7jhP612tJ?{kqz^R#=epr)uBZ?q4o$dx`n$+|6I_ccAEq{= zbwG$$>$|s}jJ=%*y!f99;4czt7?kEcfFLOu+=NV5DFd|P)UXUHu0f>HEpihKQUnC4 zaF>}-rAi6>)vjStQ8Kba6V8AM?&$39>VTizp9}14Zo>Y-5==(dnF;+@2M+dDl8MB2 zC5BmCOKDD)t7S|-d+1*+6boghfU}vbu)J5(Di~o;D#LVXW=to-zEnD)`OBJVr#9i_ zOgL`GC^&27)pl$Y^~VwV1PFpF{Jg+**L^P#1k~g97(%cdn1|V~A6e?g3LNqRPNxH5 z9toqPzXyR2Sa3R>2=6CG`M@Xbr`QyM#rFCu-zn7u?8zw|hEy#J{Y9CX#u%^9+{B3C z3NElfxavS~XWE6~UGyPbTNp=pY+=(@CH!v&!UWvR8ss-t!GZema4z^P7}^0gdFu1) z5Z^64S$_`jZEk$|!zR~ZV&&VlozSGMO88%#RTGZ2cff5ff|Ac?Ev7;a`WA&BE1{(w z7e=fM)bI0_0H*5S0l|1Z5V-#a5Pq<+6sQM>BK%_S>{^^BqByRC6u~bt9z4<*BP(J- zV!9V965r9#;JZXcd718g(1eyfmdr6$yG%DW&wPSAGV+hcivoRlBac z?rQhh{^8l3%p{Y^GryiWGda`UG7?U+o`56ou+@`m09-PliOr#dfKT&Krl?9^u{?VE zE?_Byy~nTAJEA{nOG!A*=m0hA@X#Fs>)UCor8z)03dv^SZv{Jpuze()X0;h=vh2n{ zn8v-TZZ-M?7k5L>Bheny4?MT=Xof`?Y)65+J00|Q=doATEz5Cr>Z6@LR|6q*~ zm@=z%C>uyFN_bwI-T<(w5n#g&@b6pkH^uO8_WK>z*2^+?sGNm3?s{ggr_X>~l#GV3 zDSMRor@iMqU0}JF!t3(CXY);wP>J-{@xq~cY#Bch$k_xFZw?!uE?naONy2Gu0(gq9 z4|=O;f#jkDW3vkz@+EV6S`y@RR90z6lRxaSi8-2?xqUbu1THV6+3BUsr;Qu!@iPbkF;R1RHZ zj3gxIDwrm@uIO_Du@wXIsiG(jD{yp0lZS{>;6}DOjoqY1DPY@a-|IDqi(NFD8+Spt)30%O+wF8hn5Tw z>oJh2KIgmI7_l>0a!omqP_?ur#ENYoiA+gcS~Ea;Qi3bVJgQx-Z zAZUtHJp*uh4mUA#K?sY#kYKn)LrAq68A<9W8N@goSTn()wGp|kVpGXQ)l*^^S{+Vs zl@2sy51^wBQLwEH!(KI+o*hP#kq*WH?UlS}a;mBt(W(Yfgpl<-;0m+GcLsgPR>2I? z!v%7l(#Ke{ITQl3VJv$QtQ*nM`h@fqA2KTuwsRJV08l|Xq$^8p4$qf_cCG?hS|te? z?JQ0-BWM;~LvZ3KRNHB3J2_7KIw#ZSG2#j?fRLd8E_hulr zZGzwj20{GG=-~AhKtwkLc5Edg4z)f&&GbqVQe7pq^KFD^DmFCRg;~%oNoXhz5VZ)g zv|Nq~Seh{4w~f>N+HSTNMv z0uizpMSdDJa2q34IB7ngOO^B8Y8~d{3C%=2W|th*MiNp3iLaL=_`2sfOH+q=uw=ay zc--1!1b;AwrQ$eFXce^Y#%!clp=NosEJ{ZWZHWXXu7}vx?Qx9&JzzEP>!JNGB;;}f zm`gwpn^DmOGSn7?b!kb0oU@xJg#?MK_Jf*5x^itz0_hJt+KM0@;hF@ifdtxelaWw* zsb(S|X(|cl6#$`G142Y~J$FDp7~@RKkU+dzf@extd5oaC!P?6mcEd2BZQDu`IxTHg z+CpSDAs6uP7Dy28dYrG)qRFYY7~9K~gOU{YKJ4$wl^u1M&#Vn{TZDTH`U z0>JMi47Dz@PjSit@uR>Ejfy1%vXO*B!gZrjOsxfwiUhMF!JV#2h#HV7wZdzd{UjG~ zO+sho%2bYVgoLTvC`n*VSodSS9Sv#QAPgq!mX;(4l!U%EM*=ft33l)+3CP=4jk!Ow zK=wj+OG$wEuyvRkfQaBz5~jG3gAcy=a$Rum7vFyS`MXf%^PfHX?Zc1J1it*>qg3DH z2_$s%Ijl8t6ClW&At_04v>EuiP!=xCm=-p&-$_t4mYACfO0g+Nf62^9@A zttYJe`&t*;)({9RCP*Bm1NdhWBGx^`Zi^rUo-!1@prDnlNf>JxFcDIc&;;a*Tq;Qb z(KgE!q`neb697l2hd9rEC!wbGfk3zCL#Q_qRZ0TV$My*lG;)~(u-z>sp&=i(R`x^` znjN#%(>G3w-+lPGm!4w<@WB3iKRx%|Ygk_U&i=PQy?F2;5P0mRk6wK7#YayfVPaC~ zNIVA74r%~PZGoyKH^Io|AxyNS4q`t8G0G(nr^Rn17^JN?frR=P=JFVzsf57U?gTKA zM?{)nY2IYkIgLCRLTAtcml2>X<_wYuGv7qZ5udr zz z#i=!&n$Zvm^m{2SXvqdm`@JD@9lc2NpbsSp>_5h$ltSJU8A`o~R@V^-pS0HQ=+dH% zH|H=`x`2WnBZ;jdjRJZ^JSCqNO29UP%k--Di0f$4G*YbjSxLCq^#l}ri$*OS(me^i zmA%=m{pxXi{aN^MQn6w z<=i;5!#+pzCyt$TL_)-V-0_mwKxT7k;fxh#p}iao$V#v zS&ZV?XIJ?Y<8G4l<)Vq(xt@UhJX8*;T4|hu0*$vj031{)L0P;jw)d*NW6a zeEl9xeE#GUPd@hyY`nJmfq?6jh*0JWq{;!VOc6j4s={>wtBe15dR>5DYe1H$0Q^%K zw@yK+>Lh@wHUy4mi6gfte;#dNQEMzFKdPlwyP zvIIgco6WL?3V0kX6$vlwe?ZZHg@(D`b>Aaw+<5iLj~;%S7Cd^-a}OW9@dBRaupx)-?z_JN!rj{h}L z|Lf4Yi7J2Z>yI(x? z>PaN|FOro1vcULS5VX? zSSNA)gB~H2`N-Z(Z4lz`vB(hmJhIEAqz^~RFisf>)qxs1SL+*Dw<%`nG{RHO-%%2< zwh&5gnl%ZKaLq>3kQyUeCPHm4LKO&xFe#U|M1fp!ZR>Zc{2ReCOd``>AVJkH+-S+8 zkVjEe6fr>Ju(CNqY)=qXN0#$Gq8R$Z6?YnsdGBz!NoWtIPH06m=~#9r(L8|GaEsop+hTZ6JteUNUpf9Nt@s$BRtgweap?RwJ5mVdj~?WDRHN~AiJ`E zSp1qMfgqmtf-CiRe*TX&#u3@+vgqRc1g2ZBdLYmx(jB*5*vb;qm5=|pJUTZLZrnt| zZC5=c?DB}a^BhUo9+z>aQ>%W`VA>glOCf3! zF4872qi{P%cG`L|FX24b7`MF-w*1+zk+0tS`cEwbz?5>;ulivFtf+n6L^L@+Cs zl6421UNbDRXcBPD-L4d5-VW`I5vSZOD_HKn18%;UmV($S61U+@i*kF6NP=zqAhn9b zt$Ev0c;JjNV47e@PyYmT*LgYfAT?t|5^S5_-%l{ztrrBl@R|O;1E57D=m-qdwn zk?WvF=f5bwtp0X+MM3S`;mF?k{3MbEaXi_}UiKfUR8pyQ`qJH8fS8B~ZNhKx0t+Za z9DW&S;UEbD>Varr2?_4OGZ)PqoD=`TCi@TUpR)ZLm}ORUb#`X8XXhJ}_bloaZStY3 zUUj#e`p@2$h@4?*Y|p^*%*z^GYw5Uh%b{_Xz+ z$5h-?|38rM`q@DW>BH#H2tbZsn~_;%{|;dxCAvQrBpj4e!AK+s4}^P~me0+nxtOM1 zWC&Juc$t#nWeWFmuqq5Q-~Jy+_>9idPhXY{A_SBaFnUnPu8ItT!@v42yL zaQnW4Yd_w2!^{VgHLb;&^M4~Dq=4MlV+USt4iF%C zT$@-V3_tWo{Tw_(g6^`WMP86VBr7tIFg7`_f)GiHYiuI!j$Ae^?i~nC{UZq(i-h?GAUD41bbLFP!FjuZ zdONdqBs4?XmIS0UdDs-$L5ZEwjF4gyE}{2me0oV4{x;Z^Gd95VAJK1l*~cCv`d*#`tNe@0oK&u8opk4P=}5HY(W z!5R{7lr(n{Yur|&R>i20dYGtonX4!&Mg{OP$;C`2bEt7!kl;xn(3rglcqKF;O8NQiVfgnR5&1qrM%`w0LD4))6Vr1LuopItG|w)sgC zem78znY*(OdKLgv?pNF=@V0LXi0e@xWt|aLb-P zl61$;kc6u4Zs0=_sHU-jn2mF}ia;$c2pZ8~=Bd2wHaptv$x(*2D5B5vO_J~*RE#)v za=tYFdL+PzMb@`SAf?Tk*Xv~F6v7d+M{i)nv?aIl?8&SlK~~0#72YERJYnb7tvM!4 zbQ82B0k8ACy6jhPLpsf5NM@qZ0tf;azKim~INzIfZ!T>JeWcdp7!YgNbNs3}QsHKhqK-6Ww z^}DQEcG+6tGGj|tY#Zy9vIEvm*cm2CkYJzWD$jf#EoGnMeT9ngrP%>M+o*J0p!tMAY+@OwZnak z7)&!8gr>wU(mwZ#qkRt59(BUZuQ7M;uS}gV=cmPi`i!G@ri=!(v}3RNTcCHziMWIj2-fT8Uh>yJc z3!nX@-Ck*Bjhg7db7b@Eup;U4D^QvG`*#KFrR(B&C$x0J`ew8K^Z6wL`o_ln*?R{( z5DpB(Xiggl&(@LA-n*2o#mZoUpN6{*7$TtI!&PIsmEMq=1Np(<)Gx&LM#}Rzl6PHQ~d-fbXK&Uh67jZ%>Tu&94m4t(2-yy5|RDn;WT*ceX zSUmK+?U39KhyYY`AFT%8@CM!p2|v6#2@q7~Ps4#6R2}aG2^5)>(BV4;QUV_kNAD&G zVEnp+4sQ412248;3E>LDV zg#7Z%%+tMo7CiMXxBl%W zHG?Dzq`d-?BpP*(h(DP~Rh+|$PujI5N+MuH;eJZD!NVM<2|u>pK^mFa{MYYo7O0L1 z?rJ!}F#jBQb-x9o>j#HS=mkLB@|MBM<3w>06Gt<5)|Hu`i9=(%vL5@I&qsG+BtSEA z2ZZOII!iu|?2Uq9wMBa7s^{lefZhiN(;@;!tvc0J=ECG0IQO1|4se{k z!;PE2$pjG4c$oNVOh|a9hN%TxklGO+6ZD>v$*w`H=+_LRO+@nu7+2FelMAY5-p5}u z0q5Q_h;Ta<2jM#9BRM*e*)c)P`E0vy4cuGucP6lNuCcZR($jf5mm6t*X|5|wNJMb8B+EB{F*{HB=TYbwMPvU~_VvO%;w z7!$N*Xp`E2Pm_6K!lkC%%g#XrtF6MQxF!&;vhPelU7uv|rI>)%TG4rIPYNKOX)E3{ z;okE|GTDt3;N(wtGYmzQi0jjl%y^nLB9^6}#b~apEM^^p+x=>`aBaojnV=Bno)EZ^ ze=C)9yN=^oT| zQf5N_tqG+|5W|XY51D{_$w?jb1rvB{8Kqu;iFP=<7Y1 z7v9O@QxjtCIG}V5XT-SC9y7Y#HL8Rtg50Fm7*ww|8pRd!9NSs-j&6WDkAr2X{qViAyp38Nxt zA8wsIDGSDGENPaT%T$Q=Opw>9@YA+t?PAJsV%$mJF(yOIwElRp7!SC15>3xWHAwR( zjtLi@2%+QA3t(By9iI1gCTw-1oW3}@bTUiZk(uFDYBUZ%@%&LE#-G1iuRLt`awkSwd@t>b5NN&>n5iOP%CK|)?*@)7M5YG15D5_iRdZ`k+~kSo7_zq zZ!yENFrnB6?SoKvYQ80#9H5#og2AvtB+vW8gy6x>%tg~^DL!)g5&BRJX5^+vz zhS)_gXcU_)U0K2e4wTM`2iAzig_o!zElq`R?J0v_ug$aIGJ}R*0}9ime;b-kt=icg z&>@a9+x&B5!r^O??>FS+ukVgoZ#p-tuR261#O)zOVXoS7XHd8jS`8W^NzhS9psrfE zx5NvQ(5_nlG?&oK3E~~av9CicP7n}fI|GS06VGKbxl?k1+Ok_Q5Dn9!Qu>r2HSPE* zL>)J4*c2+&9nU)ji3&}LXb{g3;WTHb6_BzT5MdRUS?bs;rE!lAa-1>Y))DR23w(IM zdtCtV`@_8Z=K>f0#|6~9Ua&X@a5{)LP zqwx=v7tKq%2l4e9!ka^7Eb0!~v3@4v8C`XP_ex8f(KvzfI$>e=Xo`fY6PzA&m~b zB}E2dFN%jrn9=_`dhw8-B)glLv?7_PUJ?{j>N^2P@+mQsD!*eKV(^8OeG=g5{J$Xy zp!)!J}4ksyZihc&X%;^hKT>a9Smx zuGXA@@)z$Ukt)@rRiXODKS`#m1D1xkvg!K-oK1+U?TIXJ1VfE%SYnw3oM(+Jv)zsn z+r`L)%xa)eAsbrzXIC|k{bFcVRrBv(2?esDnI+*9-F0H?*z5fc@DO)>mXL(#(2o(^ zckqULkOWXj0$e%WF@Ag(?&?2O?}qV4|Kk-{CE;gEhDp#dSkD?A@Mg9d4Xu{r%kS&m zaXxNtrO09oe;4z^emfbxf-`&P(wjsS#qnt3hq*b)$v=e6FcXt0X;K~oCX_cDkcTvY zfCPDU(a?;NCe(%NF4T0Pt9}T-iJ!ok0ksNNjUu)6zo6&NxpzoAznuG;+w;eA z$o}b=poE&E7Ma%5;lBT?G|P#Rcbf#&-bEjt?{A4!qNfwRF|7#_zax9-Wmi?*!SHQ} z3fFay z%v-O~Y1UKIx+HFat}O{fQotg?W>^*p6nZfwqZ|#)b_^1lk3&Z3{i@8NlRb=wnM6AX z^^CsFU(Mgxg014*xa=p#7-O<@so{;)AN#NV_tu{|G(YUKpyTshlZ30X2^S%U$k5X< zdI}4{9M`hj#FkSU`U?Gp9d7tioUc~}E2s4>rBD!plwmVlGW|~{GAgyI6mG{QVLE2b zLiy%SB(Cqi!>SyzEV%UOh^AMPz@mqFzHfUwJ8`g}Ql+D+dxH!K10;7>jwRC)8}^yU zrFJWUPMRt1?f9IoL$)^$U9y^dnIN(60_3j0ue_`Wf-UE zv7p&b^$HT`bO*1cxoxxA1oz!Q>m~5g!4y!@Iq(}c3DUYiZU7Zyrqm%+plj2N(liaU z6rM(aPVuc`T&j}&Oom?{Kt7gkXjJ%9NtF_cL1AbKG#a`04dXi&IPmJC&BjAauk91ts-IF8OEva#}}o4k_4q4o|nyW0^1)(`bzw!#u3Hy;xBs>U^8TEJc`Gwh&DhZmr7)74sR@{8Mf@4Y8XFgxrB%#rT zYAP0M(Eyzc3t`hRR(vwzBslC|kYGLHS_QFWGPX=Y-3^!F_)o$gCSlkC_Two_p^{js zt@@A_mX=~6IH8y37s`cQqfBMrzP@sd(L>LuKtw*1gjpA7SyNh_(ooBbm|zYLLnaBy zOC%^>RF0UQEs-F(;S4by+%^){-aBXjX0#ChyU^hH)xF9ufW0YTY&x?yj4JGeABfu^w?K3FlNIQH!E- zO12(koynmEN|bkF&?j3H>M=Q@@Pvq`@+A_=K#u5xcZq~N_YR$X8+zz>BP3AxB_Lxi=VToyFj=}>S`;jkEgrLqVV;^J0J8>Pa?fjj{^ZY& zo4Xykk-bPN`vJ&KuLp*_Pf2V-^w#AJXm1YDxv2}t*PjI5JsZPc&nGgC4uFFVk#OpT ztOAF_{~&B^?qHLY%q|-eLje9=5*v{+EcOOk;)Zto|Hj-w2%di`J;1%WDf*Tq*>31u zU=1X!_3bsg59l`jhv&`fir!(JyV76QlypGV{*$nIZRDLLd!`XrWS$aI=*fEufFrTk(j_5fP)J%6awH^c=c(xQ?{(yRG%bB z%th6iJ}jFhM}250w4yEjG9zT_l6A#i=MU~{&*xdt5)S;i*>yciDfBA@8_rCSU*ocJZ~*dq;YEW0X7oJ?e5{|oxupzye~#0e`L8a*YrZ6G0W4> zgU~NDwJMu!GudWijvZ|s=~Z5;8j9P+_r*#eK-|{D-!xRF!%x3JVOLy7ZDtn9jV>Tp zqW&&`UV_DvY|T;nMUI5v)15#mp+7#R`CIPixpv~{MUe#Q53;FHD8Ag74qQ&J_r(9x z%h*)&I|t5j+%UI$C+VGm2>@@WcY4!rZ@~%Y&hAwtgnNFXrnmh~!)oWS&(>Et)%LaP z(XJq2%_n5}LOaAl{?&h&$)s$ZhNJ9xo=6&b$vO>Je$plqAK5#X9i_o2j>m<*%gH}E zAx$?0HVh$w!n7uqJDVyOX()0HjIe-&%Sac5$&kq;gA3NP!MMPcui`WK1VUxN3o0g> z;N`dAv8@jSQ3c(Ub*o&|D|VGSdJTE-(l<-aWvio59;pS(l+lMj*ImKQ3)Cf9sQK_4am#yi$q?v>`=bV5uvK95(nlZPt1l30z|N7)oem=qc- z91<e93yfR%ZO5^Myggt*4JcsY##hS@n7e!lP%; zpI^U;?lr*&;+885ZW^kppGm~-nBE&14G=<=N>7*5Bh}wsO0>p3ixMH8$n(IXfUxes z6^#q`xW!+nfO_|;5a<)38q!0m5zKnyvT*@IAsFiEk3`!9EJHKk*6PJObBRBikT^|T zf}nI_fh1@k%X4}37}PU#-Rj05o}Ce6YB@P#^gOu(+iQ+7JN&c>#IQM}2a2@oFz;?( zbiYYEi7;N)tC44E>@%SZ2xiBP8>5uTGy)S6PdknNAeI;da%I-9tBGc&JgoHj?#z!h zqbah_RRAFztfA^rjZR?r#c2)9>+;)9i3q;nsFi6Mfk@vs4{B)1;Y`07sC_h{B-c>x zD4Be=bA?Szn5aV#%z4w3gqZFWQxn+~xICN9yonaX+?*Kggp((Z`79&Q0UW6bzqK3g z`n5z`PGT?Z%3PEfoPD^bVvIRDy3q+-g2SUc04sPBm3shEvI594fTtr7SXgDgTa8t3 zp6^VsYKVB`V&=+afAm*e2uv_zgF>&?#cttpm_Y8x#CBNCN?wS0FUbx+VS@DHrJJ@0uda){9z|NwdYb32 zO>o7+IhVq$4Tk~~%!HKJryxS8EkQPk=t2eIR=+cWIoC~swlSfF8&8sP^6FXx?SSGa zyn8$G)Y&$H;16m{jg<*!4m}u@J?72?O2sX`6HvA>AtSdjmCH*<5+o^>uDj-LCQfzF z1p3s3PX0g<{<)a&Huh3u<~tD<{uY>tqvjPePacZuRnR?i0esP3rx~b8IHuJA#-%VtoQ46bs6lY#HlrIBuNU(>e%R z;X>(SUk{XC1yQS8g18L!hQ`?mvD>tUYOufrZ<~m2;~*?$+XR$DiIA$Qp@Klwkvx~x z_aN$rg6T>;1KPq;F6U$-l0u_(cmTm$(U>l+B2dsr^NLz#Ejh3EgX{&@dt-qx#Q%C^=F&#>z}T(>w)_S6N8DcVY7wl zYxukwWg_V`%Zy04eQxheSM5hNyUSrhMD+#}Cr{n>{;AW_n(1SK5M7=r8tj;~XgA%U z9js-O_FPCR9JTL&&z?#|)uzUTS*hKWn53&q=6W{IEH2xMrd%os@Ri!<_2vnc6(=3Uv6^s9&vFNNW77Vs&7Zv3@E*PPiUw>){`4?x zzut}ZC!6nYm)Lyr>D~a-ZsEH(3_6sftP}42e{S<1&hb8H=|4Z>IIk?-yZ_&WKbv1x zCp`H7k6<6zyQ3689~|BZBcIV86pzcU8FD$N+v#fSiwAYz5F}jAyx>5Z7uwfYC8xvUHN$ z5%IBi$1DiLKp2MGQCu8E5d`N3j*!OYx;gq|{ga9}2{(lcA`UJuO-p-emrAA} zTC5H_$_tJRT}ez7gz@SCe`g&~0-yvy31Bk$f)f5vlM*O1FVGQwWePDUEan|_kr+k^#fNjt#C zC1B#gt2vvNfPn|UieU+uckpcH*tCR*XBfRG(QNNE%tgAEsL?xLM8x9YhQX|~IAQ+; z-_#PG6(yW;<=~rH3D*^;39c2bgndN`w-}qLN&b5i-t3*vPa; zJ=EH!n|8O|Y!7W;zTx#-9N7*-n!uD+LBK=>1TVM>LVyZr5b=Nszl4L4=n@m`!CR9( zbWIQJY5#~l_R>G4@6BMfqN}S|8}t)lzVp8C`<~=|G6-YxOuhN&!0D|CqB(VV2p!j<@>rzTW4 zN1ywFZ?y8q=1TI-VwT5Rs`cH`l2lae#=?K%o$v7+}iC0B<=J&0DTtfc;z zTY}%b2~YLHmCzY_Xo9iP2&#L-L3}R>)Uvk;#O9Ou`jmd1+2ba-*HY(f{{zbols5_i zlpC+f)iGvKuEgVqZp&TxAN?~-_&~TKKlk#QKta+>@P^HlSY|c>@_sL{Om4n8%1vm@ z?3bcOdPZp}jT_S|`*8)2HuD9>QT9|!sA`iMz{xyFMN|lI{^{Ju7l&>WhCdgs$Y+L| z*e?=*#mU7mP-X%pP~Ee)R1+rG63?Ew%%lrgM+^hQUcd@}e*jWmUM|tj`euUpS)gic z*$<|uB^4z^Dy4`epJ#1mdoUm6`Kf#Xm4&FyQZCzwn&5?fdwJ+KVMHff5#5cLz>)l~ zUQ=x^E#ICSv}=NEX8KL;`5Wl2tYF$l*-;7?m!?=s^RHH~OZ`=c%yGhnR7FvJppY`@-U?=oRTxCY<8 zlL^3}(#0mg zAUE1o57iJiK~j*R*uh)Cb(bsLPGb%th#6KOO=1zk0jvU{vxSZcL3K7*c;Qc^5tBfD zb!Gz5Okjy5OE?f$?uH5MgTu8L{Qi@= zd^v-S+~k(nt&rrwh=meKlG+^;4tE(#yDerypv+3eRUEgut+;a91hXGw?tC)QWr8$= z1y??Ka7Roqe4rPu%a>0tVFD%Vi0H{nMoP)Kh&J_E(h!N!CUz2%QFFyCovGOY2KFhN zg|2cFf-#IsHpym_M93j7Q$j6%tU$AXan@-zaBKpN%9x-OCc8|q$Z~@va(BdpZyyWS zM*oT7ViRCz46#z8ENjj;MX)<@m)YZ$(GR|H94`@4mQR?F zM0p*^Uy&{oN@#t-6L-S|!y};w4E<)f$OLLmAO^fZ0d7PF$jRt%x8gI1#WFy444pe$ z?Lr_6;745F4%bmh0lbuPrkUNt6DIha=*)T#5Yc|>-66)u;EB2ZH^J~g=n0R%HC$i< z1f7U4L0Yh9W+{-~1M4y5Y!sJ)oi#$p2>5-J(8w4I<#ZDWwMDEfj%n`>b#;l2I|W1> zbIRCaT7<9$1l(54fEeE3irPeA((qWNW4))f86%=bF zN?^IDc&f>RbhA&tWHj|!1kP8#55Ls}+b&|D7(f+e|0YzRpz^OXEj|v7`@DTY#(Cg9 z=y95AUH*Bd|0MeNcBJV@goi>88GLW}Ehf+@KXPJw4rJ7Pl-veTb?jDGS_uqP`*n5S z-2{W-3!%3>G#Jh`0ZKE7Xst9e!QB`OUIMudAR9t!NXEc)pT6^7!Gw{I9}B(a@h2na zny}zOM85?XE*5!i>rU@hZrq_n;^MstnhA#ChXX?I(Lefr_#6|;%mldwEToni;^=MN z)B!P-w_E!6pHJS=gprSjgqvaL!3dmdLM@}_L^tBb4v^b{0^EDhf&TodPPj21zYmmj z0i`0MCk2GlM6|l@|6;ps03HZ8%g~3Ep844!HV1^0MGS=RO}Je^dMNw{eD*#$h4CdK zYRfMjB09bICfp|CN5X%{qxU7rWD;#Qo5&#BUq5;Br(|Sd=9?!^W@d?aCboYgYMhM3 zlbFOyW8y!@Ri?|;m>4JOk783NG-$c%-$c^?aPpTK$-!+Q^CQ)(mfhZ^w)t?r(C{%#Kr2><{900jMi?F|C}0000001E-}OqT|irD`Dn O0000 @@ -16,23 +16,47 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center_vertical" - android:orientation="horizontal" - android:paddingVertical="16dp"> + android:paddingTop="12dp" + android:paddingBottom="8dp"> + + + + + + + + - + android:paddingBottom="12dp"> - + android:background="@drawable/background_widget_item_timetable" + android:backgroundTint="?attr/colorSurface" + android:gravity="center_vertical" + android:minHeight="48dp" + android:orientation="horizontal" + android:paddingHorizontal="12dp" + android:paddingVertical="8dp"> - + + + + + + + + + + + + + + + + + + + + + + + + + + + android:background="@drawable/background_widget_item_timetable" + android:backgroundTint="?attr/colorSurface" + android:gravity="center_vertical" + android:minHeight="48dp" + android:orientation="horizontal" + android:paddingHorizontal="12dp" + android:paddingVertical="8dp"> - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + android:layout_height="wrap_content" + android:background="@drawable/background_widget_item_timetable" + android:backgroundTint="?attr/colorSurface" + android:gravity="center_vertical" + android:minHeight="48dp" + android:orientation="horizontal" + android:paddingHorizontal="12dp" + android:paddingVertical="8dp"> + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_widget_timetable.xml b/app/src/main/res/layout/item_widget_timetable.xml index 27c9db66..01f4525e 100644 --- a/app/src/main/res/layout/item_widget_timetable.xml +++ b/app/src/main/res/layout/item_widget_timetable.xml @@ -6,11 +6,12 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@drawable/background_widget_item_timetable" - android:backgroundTint="?attr/backgroundColor" + android:backgroundTint="?attr/colorSurface" android:gravity="center_vertical" + android:minHeight="48dp" android:orientation="horizontal" - android:paddingHorizontal="16dp" - android:paddingVertical="12dp" + android:paddingHorizontal="12dp" + android:paddingVertical="8dp" android:theme="@style/Wulkanowy.Widget.Theme" tools:context=".ui.modules.timetablewidget.TimetableWidgetFactory"> @@ -18,15 +19,14 @@ android:id="@+id/timetableWidgetItemNumber" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:textAppearance="?attr/textAppearanceHeadline6" - android:textSize="24sp" + android:textSize="22sp" tools:text="1" tools:textColor="?attr/colorTimetableChange" /> @@ -41,7 +41,7 @@ android:id="@+id/timetableWidgetItemTimeFinish" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginTop="4dp" + android:layout_marginTop="2dp" android:textAppearance="?attr/textAppearanceBodySmall" tools:text="09:45" /> @@ -60,7 +60,7 @@ android:layout_height="wrap_content" android:ellipsize="end" android:lines="1" - android:textAppearance="?attr/textAppearanceTitleMedium" + android:textSize="14sp" tools:text="Programowanie aplikacji mobilnych i desktopowych" /> + tools:context=".ui.modules.timetablewidget.TimetableWidgetProvider" + tools:targetApi="s"> + android:paddingTop="12dp" + android:paddingBottom="8dp"> + + + + + + + + + android:textSize="18sp" + tools:text="Friday, 19.05" /> - - - - - - @@ -111,5 +114,7 @@ android:text="@string/widget_timetable_no_items" android:textAppearance="?attr/textAppearanceBody1" android:visibility="gone" /> + + diff --git a/app/src/main/res/xml/provider_widget_timetable.xml b/app/src/main/res/xml/provider_widget_timetable.xml index 3cdad0c8..555d8cb1 100644 --- a/app/src/main/res/xml/provider_widget_timetable.xml +++ b/app/src/main/res/xml/provider_widget_timetable.xml @@ -3,15 +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="2" + android:targetCellHeight="3" android:updatePeriodMillis="3600000" android:widgetCategory="home_screen" tools:targetApi="s" /> From 91d7ee442edc87b045ae2693874d612bf0bcfe87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Wed, 26 Jul 2023 19:37:06 +0200 Subject: [PATCH 21/38] New Crowdin updates (#2257) --- .../res/values-it-rIT/preferences_values.xml | 65 ++ app/src/main/res/values-it-rIT/strings.xml | 747 ++++++++++++++++++ app/src/main/res/values-uk/strings.xml | 6 +- 3 files changed, 815 insertions(+), 3 deletions(-) create mode 100644 app/src/main/res/values-it-rIT/preferences_values.xml create mode 100644 app/src/main/res/values-it-rIT/strings.xml 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 00000000..ac2b6e9e --- /dev/null +++ b/app/src/main/res/values-it-rIT/preferences_values.xml @@ -0,0 +1,65 @@ + + + + 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 + + + Lucky number + Unread messages + Attendance + Lessons + Grades + Homework + School announcements + Exams + Conferences + + diff --git a/app/src/main/res/values-it-rIT/strings.xml b/app/src/main/res/values-it-rIT/strings.xml new file mode 100644 index 00000000..5c7d02a0 --- /dev/null +++ b/app/src/main/res/values-it-rIT/strings.xml @@ -0,0 +1,747 @@ + + + + Login + Wulkanowy + Grades + Attendance + Exams + Timetable + Settings + More + About + Log viewer + Debug + Notification debug + Contributors + Licenses + Messages + New message + New homework + Notes and achievements + Homework + Accounts manager + Select account + Account details + Student info + Dashboard + Notifications center + Menu configuartion + + Semester %1$d, %2$d/%3$d + + Sign in with the student or parent account + Enter the symbol from the register page for account: <b>%1$s</b> + Username + Email + Login, PESEL or e-mail + Password + UONET+ register variant + Custom domain suffix + Mobile API + Scraper + Hybrid + Token + PIN + Symbol + Sign in + Password too short + Login details are incorrect + %1$s. Make sure the correct UONET+ register variation is selected below + Invalid PIN + Invalid token + Token expired + Invalid email + Use the assigned login instead of email + Use the assigned login or email in @%1$s + Invalid symbol + Student not found. Validate the symbol and the chosen variation of the UONET+ register + Selected student is already logged in + The symbol can be found on the register page in Uczeń → Dostęp Mobilny → Wygeneruj kod dostępu.\n\nMake sure that you have set the appropriate register variant in the UONET+ register variant field on the first login screen + Select students to log in to the application + Other options + In this mode, a lucky number does not work, a class grade stats, summary of attendance, excuse for absence, completed lessons, school information and preview of the list of registered devices + This mode displays the same data as it appears on the register website + The combination of the best features of the other two modes. It works faster than scraper and provides features not available in the Mobile API mode. It is in the experimental phase + Privacy policy + Trouble signing in? Contact us! + Email + Discord + Send email + Make sure you select the correct UONET+ register variation! + I forgot my password + Recover your account + Recover + Student is already signed in + Standard + Other search locations + No active students found + Enter a different symbol + + Enable notifications + Enable notifications so you don\'t miss message from teacher or new grade + Skip + Enable + + Account manager + Log in + Session expired + Session expired, log in again + Application support + Do you like this app? Support its development by enabling non-invasive ads that you can disable at any time + Enable ads + + Grade + Semester %d + Change semester + No grades + Weight + Weight: %s + Comment + Number of new ratings: %1$d + Average: %1$.2f + Points: %s + No average + Total points + Final grade + Predicted grade + Calculated average + How does Calculated Average work? + The Calculated Average is the arithmetic average calculated from the subjects averages. It allows you to know the approximate final average. It is calculated in a way selected by the user in the application settings. It is recommended that you choose the appropriate option. This is because the calculation of school averages differs. Additionally, if your school reports the average of the subjects on the Vulcan page, the application downloads them and does not calculate these averages. This can be changed by forcing the calculation of the average in the application settings.\n\nAverage of grades only from selected semester:\n1. Calculating the weighted average for each subject in a given semester\n2.Adding calculated averages\n3. Calculation of the arithmetic average of the summed averages\n\nAverage of averages from both semesters:\n1.Calculating the weighted average for each subject in semester 1 and 2\n2. Calculating the arithmetic average of the calculated averages for semesters 1 and 2 for each subject.\n3. Adding calculated averages\n4. Calculation of the arithmetic average of the summed averages\n\nAverage of grades from the whole year:\n1. Calculating weighted average over the year for each subject. The final average in the 1st semester is irrelevant.\n2. Adding calculated averages\n3. Calculating the arithmetic average of summed averages + How does the Final Average work? + The Final Average is the arithmetic average calculated from all currently available final grades in the given semester.\n\nThe calculation scheme consists of the following steps:\n1. Summing up the final grades given by teachers\n2. Divide by the number of subjects that have already been graded + Final average + from %1$d of %2$d subjects + Summary + Class + Mark as read + Partial + Semester + Points + Legend + Class average: %1$s + Your average: %1$s + Your grade: %1$s + Class + Student + + %d grade + %d grades + + + New grade + New grades + + + New predicted grade + New predicted grades + + + New final grade + New final grades + + + You received %1$d grade + You received %1$d grades + + + You received %1$d predicted grade + You received %1$d predicted grades + + + You received %1$d final grade + You received %1$d final grades + + + Lesson + Room + Group + Hours + Changes + No lessons this day + %s min + %s sec + %1$s left + in %1$s + Finished + Now: %s + Next: %s + Later: %s + %1$s lesson %2$d - %3$s + Change of room from %1$s to %2$s + Change of teacher from %1$s to %2$s + Change of subject from %1$s to %2$s + + Timetable change + Timetable changes + + + %1$s - %2$d change in timetable + %1$s - %2$d changes in timetable + + + %1$d change in timetable + %1$d changes in timetable + + + %d change + %d changes + + + Completed lessons + Show completed lessons + No info about completed lessons + Topic + Absence + Resources + + Additional lessons + Show additional lessons + No info about additional lessons + New lesson + New additional lesson + Additional lesson added successfully + Additional lesson deleted successfully + Repeat weekly + Delete additional lesson + Just this lesson + All in the series + Start time + End time + End time must be greater than start time + + Attendance summary + Absent for school reasons + Excused absence + Unexcused absence + Exemption + Excused lateness + Unexcused lateness + Present + Deleted + Unknown + Number of lesson + No entries + Absence reason (optional) + Send + Absence excuse request sent successfully! + You must select at least one absence! + Excuse + + New attendance + New attendance + + + %1$d new attendance + %1$d attendance + + + %d attendance + %d attendance + + + Total + + No exams this week + Type + Entry date + + New exam + New exams + + + %d new exam + %d new exams + + + %d exam + %d exams + + + Inbox + Sent + Trash + (no subject) + No messages + From: + To: + Date: %1$s + Reply + Forward + Select all + Unselect all + Move to trash + Delete permanently + Message deleted successfully + student + parent + guardian + employee + Share + Print + Subject + Content + Message sent successfully + Message does not exist + You need to choose at least 1 recipient + The message content must be at least 3 characters + All mailboxes + Only unread + Only with attachments + Read: %s + Read by: %1$d of %2$d people + + %1$d message + %1$d messages + + + New message + New messages + + Do you want to restore draft message? + Do you want to restore draft message with recipients: %s? + + You received %1$d message + You received %1$d messages + + + %1$d selected + %1$d selected + + Messages deleted + Choose mailbox + + No info about notes + Points + + %d note + %d notes + + + New note + New notes + + + You received %1$d note + You received %1$d notes + + + + %d praise + %d praises + + + New praise + New praises + + + You received %1$d praise + You received %1$d praises + + + + %d neutral note + %d neutral notes + + + New neutral note + New neutral notes + + + You received %1$d neutral note + You received %1$d neutral notes + + + No info about homework + Mark as done + Mark as undone + Add homework + Homework added successfully + Homework deleted successfully + Attachments + + New homework + New homework + + + You received %d new homework + You received %d new homework + + + %d homework + %d homework + + + Lucky number + Today\'s lucky number is + No info about the lucky number + Lucky number for today + Today\'s lucky number is: %s + Show history + + Lucky number history + No info about lucky numbers + + Mobile devices + No devices + Deregister + Device removed + QR code + Token + Symbol + PIN + + School and teachers + + School + No info about school + School name + School address + Telephone + Name of headmaster + Name of pedagogue + Show on map + Call + + Teachers + No info about teachers + No subject + + Conferences + No info about conferences + + %d conference + %d conferences + + + New conference + New conferences + + + You have %1$d new conference + You have %1$d new conferences + + Present at conference + Agenda + Place + Topic + + School announcements + No school announcements + + %d school announcement + %d school announcements + + + New school announcement + New school announcements + + + You have %1$d new school announcement + You have %1$d new school announcements + + + Add account + Logout + Do you want to log out this student? + Student logout + Student account + Parent account + Edit data + Accounts manager + Select student + Family + Contact + Residence details + Personal information + + App version + Contributors + List of Wulkanowy developers + Report a bug + Send a bug report via e-mail + FAQ + Read Frequently Asked Questions + Discord server + Join the Wulkanowy community + Facebook fanpage + Twitter page + Follow us on twitter + Like our facebook fanpage + Privacy policy + Rules for collecting personal data + System settings + Open system settings + Homepage + Visit the website and help develop the application + Licenses + Licenses of libraries used in the application + + License + + Avatar + See more on GitHub + + No info about student or student family + Name + Second name + Gender + Polish citizenship + Family name + Mother\'s and father\'s names + Phone + Cellphone + E-mail + Address of residence + Address of registration + Correspondence address + Surname and first name + Degree of kinship + Address + Phones + Male + Female + Last name + Guardian + + Nick + Add nick + Choose avatar color + + Share logs + Refresh + + Lessons + (Tomorrow) + (Today and tomorrow) + In a moment: + Soon: + First: + Now: + End of lessons + Next: + Later: + + %1$d more lesson + %1$d more lessons + + until %1$s + No upcoming lessons + An error occurred while loading the lessons + Homework + No homework to do + An error occurred while loading the homework + + %1$d more homework + %1$d more homework + + due %1$s + Last grades + No new grades + An error occurred while loading the grades + School announcements + No current announcements + An error occurred while loading the announcements + + %1$d more announcement + %1$d more announcements + + Exams + No upcoming exams + An error occurred while loading the exams + + %1$d more exam + %1$d more exams + + Conferences + No upcoming conferences + An error occurred while loading the conferences + + %1$d more conference + %1$d more conferences + + An error occurred while loading data + None + + Check for updates + Before reporting a bug, check first if an update with the bug fix is available + + Content + Retry + Description + No description + Teacher + Date + Entry date + Color + Details + Category + Close + No data + Subject + Prev + Next + Search + Search… + Yes + No + Save + Title + Add + Copied + Undo + Change + Add to calendar + Cancel + + No lessons + Synchronized on %1$s at %2$s + Choose theme + Light + Dark + System Theme + + App + Default view + Calculated average options + Force average calculation by app + Show presence + Theme + Grades expanding + Mark current lesson + Show groups next to subjects + Show chart list in class grades + Show subjects without grades + Grades color scheme + Subjects sorting + Language + Menu configuration + Set the order of functions in the menu + Notifications + Other + Show notifications + Show upcoming lesson notifications + Make upcoming lesson notification persistent + Turn off when notification is not showing in your watch/band + Open system notification settings + Fix synchronization & notifications issues + Your device may have data synchronization issues and with notifications.\n\nTo fix them, you need to add Wulkanowy to the autostart and turn off battery optimization/saving in the phone settings. + Show debug notifications + Synchronization is disabled + Official app notifications + Capture official app notifications + Remove official app notifications after capture + Capture notifications + With this feature you can gain a substitute of push notifications like in the official app. All you need to do is allow Wulkanowy to receive all notifications in your system settings.\n\nHow it works?\nWhen you get a notification in Dziennik VULCAN, Wulkanowy will be notified (that\'s what these extra permissions are for) and will trigger a sync so that can send its own notification.\n\nFOR ADVANCED USERS ONLY + Upcoming lesson notifications + You must allow the Wulkanowy app to set alarms and reminders in your system settings to use this feature. + Go to settings + Synchronization + Automatic update + Suspended on holidays + Updates interval + Wi-Fi only + Sync now + Synced! + Sync failed + Sync in progress + Last full sync: %s + Value of the plus + Value of the minus + Reply with message history + Show arithmetic average when no weights provided + Support + Privacy Policy + Agreements + Consent to processing of data related to ads + Show ads in app + Watch single ad to support project + Consent to data processing + To view an advertisement you must agree to the data processing terms of our Privacy Policy + Agree + Privacy policy + Ad is loading + Thank you for your support, come back later for more ads + Can we use your data to display ads? + You can change your choice anytime in the app settings. We may use your data to display ads tailored to you or, using less of your data, display non-personalized ads. Please see our Privacy Policy for details + Personalized ads + Non-personalized ads + I am over 18 years old + Yes, personalized ads + Yes, non-personalized ads + Advanced + Appearance & Behavior + Notifications + Synchronization + Advertisements + Grades + Dashboard + Tiles visibility + Attendance + Timetable + Grades + Calculated average + Messages + Appearance & Behavior + Languages, themes, subjects sorting + App notifications, fix problems + Notifications + Synchronization + Automatic update, synchronization interval + Plus and minus values, average calculation + Advanced + App version, contributors, social portals + Displaying advertisements, project support + + New grades + New homework + New conferences + New exams + Lucky number + New messages + New notes + New school announcements + Push notifications + Upcoming lessons + Debug + Timetable change + New attendance + + Black + Red + Blue + Green + Purple + No color + + Download of updates has started… + An update has just been downloaded. + Restart + Update failed! Wulkanowy may not function properly. Consider updating + + Application restart + The application must restart for the changes to be saved + 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 + + No internet connection + An error occurred. Check your device clock + Connection to register failed. Servers can be overloaded. Please try again later + Loading data failed. Please try again later + Register password change required + Maintenance underway UONET + register. Try again later + Unknown UONET + register error. Try again later + Unknown application error. Please try again later + An unexpected error occurred + Feature disabled by your school + Feature not available. Login in a mode other than Mobile API + This field is required + diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 30a587cc..f89e38fc 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -37,7 +37,7 @@ Логін, PESEL або e-mail Пароль Тип щоденника UONET+ - Custom domain suffix + Користувацький суфікс домену Мobile API Scraper Hybrid @@ -814,8 +814,8 @@ Авторизацію відхилено. Надані дані не збігаються із записами в кабінеті секретаря. Неправильний PESEL Число PESEL - Authorize - Authorization completed successfully + Авторизовать + Авторизація пройшла успішно Авторизувати Для роботи програми нам потрібно підтвердити вашу особу. Будь ласка, введіть число PESEL <b>%1$s</b> студента в поле нижче Поки що пропустити From 64cc24ae6012470b59c9e7038c2145147f09c29a Mon Sep 17 00:00:00 2001 From: Mateusz Idziejczak Date: Wed, 26 Jul 2023 22:17:58 +0200 Subject: [PATCH 22/38] Add incognito mode in messages (#1970) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mikołaj Pich --- .../data/repositories/MessageRepository.kt | 22 ++++++++++++++----- .../repositories/PreferencesRepository.kt | 6 +++++ .../ui/modules/message/MessageFragment.kt | 9 +++++++- .../ui/modules/message/MessagePresenter.kt | 13 ++++++++++- .../ui/modules/message/MessageView.kt | 3 +++ .../message/preview/MessagePreviewFragment.kt | 5 +++++ .../preview/MessagePreviewPresenter.kt | 13 ++++++++++- .../message/preview/MessagePreviewView.kt | 3 +++ .../modules/message/tab/MessageTabFragment.kt | 17 +++++++++----- .../message/tab/MessageTabPresenter.kt | 5 +++-- .../ui/modules/message/tab/MessageTabView.kt | 3 ++- .../main/res/values/preferences_defaults.xml | 1 + app/src/main/res/values/preferences_keys.xml | 1 + app/src/main/res/values/strings.xml | 5 +++++ .../res/xml/scheme_preferences_advanced.xml | 7 ++++++ 15 files changed, 96 insertions(+), 17 deletions(-) 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 53d9bead..c8fccb23 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 @@ -3,18 +3,26 @@ package io.github.wulkanowy.data.repositories import android.content.Context import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R -import io.github.wulkanowy.data.* +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 @@ -25,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 @@ -97,7 +104,7 @@ 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) @@ -113,7 +120,10 @@ class MessageRepository @Inject constructor( 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 } @@ -123,7 +133,7 @@ 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") } ) 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 348a4054..1b489340 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 @@ -343,6 +343,12 @@ class PreferencesRepository @Inject constructor( ) } + 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) } 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 4317fb7f..02bc13a1 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,7 +11,9 @@ 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 @@ -49,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) } @@ -95,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() 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 cf6bad19..37a2d422 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) { 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 def4a275..7fdc6e18 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) 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 6c54d9fc..3ed685cd 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,6 +12,7 @@ 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 @@ -164,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 56f23b6f..cd7b7284 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,11 +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.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 @@ -20,6 +22,7 @@ class MessagePreviewPresenter @Inject constructor( errorHandler: ErrorHandler, studentRepository: StudentRepository, private val messageRepository: MessageRepository, + private val preferencesRepository: PreferencesRepository, private val analytics: AnalyticsHelper ) : BasePresenter(errorHandler, studentRepository) { @@ -54,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 { @@ -65,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 { 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 c5a94793..7f5f140b 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/tab/MessageTabFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabFragment.kt index 592cbd60..4364e868 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,6 +7,7 @@ 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 @@ -134,14 +135,20 @@ class MessageTabFragment : BaseFragment(R.layout.frag } } + @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 { @@ -207,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) { 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 ec92e9c2..90f93b14 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,5 +1,6 @@ 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 @@ -26,7 +27,7 @@ class MessageTabPresenter @Inject constructor( errorHandler: ErrorHandler, studentRepository: StudentRepository, private val messageRepository: MessageRepository, - private val analytics: AnalyticsHelper + private val analytics: AnalyticsHelper, ) : BasePresenter(errorHandler, studentRepository) { lateinit var folder: MessageFolder @@ -135,7 +136,7 @@ class MessageTabPresenter @Inject constructor( messageRepository.deleteMessages(student, selectedMailbox, messageList) } .onFailure(errorHandler::dispatch) - .onSuccess { view?.showMessagesDeleted() } + .onSuccess { view?.showMessage(R.string.message_messages_deleted) } } } 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 6ece6621..247af434 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,6 @@ 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 @@ -26,7 +27,7 @@ interface MessageTabView : BaseView { fun showEmpty(show: Boolean) - fun showMessagesDeleted() + fun showMessage(@StringRes messageId: Int) fun showErrorView(show: Boolean) diff --git a/app/src/main/res/values/preferences_defaults.xml b/app/src/main/res/values/preferences_defaults.xml index 6c81100d..fefd9b13 100644 --- a/app/src/main/res/values/preferences_defaults.xml +++ b/app/src/main/res/values/preferences_defaults.xml @@ -38,4 +38,5 @@ false false + false diff --git a/app/src/main/res/values/preferences_keys.xml b/app/src/main/res/values/preferences_keys.xml index 716639c0..e7fa542a 100644 --- a/app/src/main/res/values/preferences_keys.xml +++ b/app/src/main/res/values/preferences_keys.xml @@ -39,5 +39,6 @@ ads_privacy_policy ads_consent_data_processing ads_over_eighteen + incognito_mode appearance_menu_order diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 98c316cb..9dc7e796 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -339,6 +339,8 @@ Messages deleted Choose mailbox + Incognito mode is on + Thanks to incognito mode sender is not notified when you read the message @@ -728,6 +730,9 @@ Value of the minus Reply with message history Show arithmetic average when no weights provided + Incognito mode + Do not inform about reading the message + Support Privacy Policy Agreements diff --git a/app/src/main/res/xml/scheme_preferences_advanced.xml b/app/src/main/res/xml/scheme_preferences_advanced.xml index 95f6f383..8185de81 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" /> + From 7f6a13a9ee8cae095855b9798b4220b5bd127299 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Jul 2023 17:35:29 +0000 Subject: [PATCH 23/38] Bump coroutines from 1.7.2 to 1.7.3 (#2267) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index f8603cc8..e188d709 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -187,7 +187,7 @@ ext { room = "2.5.2" chucker = "3.5.2" mockk = "1.13.5" - coroutines = "1.7.2" + coroutines = "1.7.3" } dependencies { From fc2adff997dd24f2fc75d3cf7bcc60a564bf3716 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Jul 2023 17:35:38 +0000 Subject: [PATCH 24/38] Bump androidx.fragment:fragment-ktx from 1.6.0 to 1.6.1 (#2269) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index e188d709..d43b667c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -202,7 +202,7 @@ dependencies { implementation 'androidx.core:core-splashscreen:1.0.1' implementation "androidx.activity:activity-ktx:1.7.2" implementation "androidx.appcompat:appcompat:1.6.1" - implementation "androidx.fragment:fragment-ktx:1.6.0" + implementation "androidx.fragment:fragment-ktx:1.6.1" implementation "androidx.annotation:annotation:1.6.0" implementation "androidx.preference:preference-ktx:1.2.0" From 0f129109ba25cace9a0e09cc4ed7d26668daa5ab Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Jul 2023 19:00:17 +0000 Subject: [PATCH 25/38] Bump com.android.tools.build:gradle from 8.0.2 to 8.1.0 (#2266) --- app/build.gradle | 26 ++++++++++++------------ build.gradle | 6 +++--- gradle/wrapper/gradle-wrapper.properties | 2 +- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index d43b667c..74ef02bc 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,3 +1,6 @@ +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' @@ -17,7 +20,7 @@ apply from: 'hooks.gradle' android { namespace 'io.github.wulkanowy' - compileSdkVersion 33 + compileSdk 33 defaultConfig { applicationId "io.github.wulkanowy" @@ -78,7 +81,7 @@ android { } } - flavorDimensions "platform" + flavorDimensions += "platform" productFlavors { hms { @@ -117,20 +120,20 @@ 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"] } @@ -152,14 +155,11 @@ kapt { ksp { arg("room.schemaLocation", "$projectDir/schemas".toString()) } -kotlin { - jvmToolchain(11) -} play { defaultToAppBundles = false track = 'production' - releaseStatus = com.github.triplet.gradle.androidpublisher.ReleaseStatus.IN_PROGRESS + releaseStatus = ReleaseStatus.IN_PROGRESS userFraction = 0.25d updatePriority = 1 enabled.set(false) @@ -172,7 +172,7 @@ huaweiPublish { buildFormat = "aab" deployType = "publish" releaseNotes = [ - new ru.cian.huawei.publish.ReleaseNote( + new ReleaseNote( "pl-PL", "$projectDir/src/main/play/release-notes/pl-PL/default.txt" ) diff --git a/build.gradle b/build.gradle index 9584caac..c5a6f598 100644 --- a/build.gradle +++ b/build.gradle @@ -14,12 +14,12 @@ buildscript { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" classpath "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin:$kotlin_version-1.0.11" - classpath 'com.android.tools.build:gradle:8.0.2' + classpath 'com.android.tools.build:gradle:8.1.0' classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" classpath 'com.google.gms:google-services:4.3.15' classpath 'com.huawei.agconnect:agcp:1.9.1.300' classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.7' - classpath "com.github.triplet.gradle:play-publisher:3.6.0" + classpath "com.github.triplet.gradle:play-publisher:3.8.4" classpath "ru.cian:huawei-publish-gradle-plugin:1.4.0" classpath "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:4.3.0.3225" classpath "gradle.plugin.com.star-zero.gradle:githook:1.2.0" @@ -37,6 +37,6 @@ allprojects { } } -task clean(type: Delete) { +tasks.register('clean', Delete) { delete rootProject.buildDir } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 8707e8b5..9b0a13f0 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-all.zip networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From 50326c7a48644d6f5997df130d8d88a1496d7188 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Jul 2023 19:00:37 +0000 Subject: [PATCH 26/38] Bump androidx.recyclerview:recyclerview from 1.3.0 to 1.3.1 (#2268) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 74ef02bc..197ea356 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -206,7 +206,7 @@ dependencies { implementation "androidx.annotation:annotation:1.6.0" implementation "androidx.preference:preference-ktx:1.2.0" - implementation "androidx.recyclerview:recyclerview:1.3.0" + implementation "androidx.recyclerview:recyclerview:1.3.1" implementation "androidx.viewpager2:viewpager2:1.1.0-beta02" implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" implementation "androidx.constraintlayout:constraintlayout:2.1.4" From 74820f9571cb9cf6259f45b73409552277ae975a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Mon, 31 Jul 2023 21:32:07 +0200 Subject: [PATCH 27/38] New Crowdin updates (#2265) --- app/src/main/res/values-cs/strings.xml | 6 +++++- app/src/main/res/values-da-rDK/strings.xml | 4 ++++ app/src/main/res/values-de/strings.xml | 4 ++++ app/src/main/res/values-es-rES/strings.xml | 4 ++++ app/src/main/res/values-it-rIT/strings.xml | 4 ++++ app/src/main/res/values-pl/strings.xml | 4 ++++ app/src/main/res/values-ru/strings.xml | 4 ++++ app/src/main/res/values-sk/strings.xml | 6 +++++- app/src/main/res/values-uk/strings.xml | 4 ++++ 9 files changed, 38 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 964329da..ff461d42 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -37,7 +37,7 @@ Přihlášení, číslo PESEL nebo e-mail Heslo Variace deníku UONET+ - Custom domain suffix + Vlastní přípona domény Mobile API Scraper Hybridní @@ -352,6 +352,8 @@ 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 @@ -738,6 +740,8 @@ 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 diff --git a/app/src/main/res/values-da-rDK/strings.xml b/app/src/main/res/values-da-rDK/strings.xml index 5c7d02a0..259a4264 100644 --- a/app/src/main/res/values-da-rDK/strings.xml +++ b/app/src/main/res/values-da-rDK/strings.xml @@ -310,6 +310,8 @@ Messages deleted Choose mailbox + Incognito mode is on + Thanks to incognito mode sender is not notified when you read the message No info about notes Points @@ -650,6 +652,8 @@ Value of the minus Reply with message history Show arithmetic average when no weights provided + Incognito mode + Do not inform about reading the message Support Privacy Policy Agreements diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 96423e35..1836d047 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -310,6 +310,8 @@ 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 @@ -650,6 +652,8 @@ 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 Datenschutz-Bestimmungen Vereinbarungen diff --git a/app/src/main/res/values-es-rES/strings.xml b/app/src/main/res/values-es-rES/strings.xml index 5c7d02a0..259a4264 100644 --- a/app/src/main/res/values-es-rES/strings.xml +++ b/app/src/main/res/values-es-rES/strings.xml @@ -310,6 +310,8 @@ Messages deleted Choose mailbox + Incognito mode is on + Thanks to incognito mode sender is not notified when you read the message No info about notes Points @@ -650,6 +652,8 @@ Value of the minus Reply with message history Show arithmetic average when no weights provided + Incognito mode + Do not inform about reading the message Support Privacy Policy Agreements diff --git a/app/src/main/res/values-it-rIT/strings.xml b/app/src/main/res/values-it-rIT/strings.xml index 5c7d02a0..259a4264 100644 --- a/app/src/main/res/values-it-rIT/strings.xml +++ b/app/src/main/res/values-it-rIT/strings.xml @@ -310,6 +310,8 @@ Messages deleted Choose mailbox + Incognito mode is on + Thanks to incognito mode sender is not notified when you read the message No info about notes Points @@ -650,6 +652,8 @@ Value of the minus Reply with message history Show arithmetic average when no weights provided + Incognito mode + Do not inform about reading the message Support Privacy Policy Agreements diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 2c707797..0c1bbf78 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -352,6 +352,8 @@ 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 @@ -738,6 +740,8 @@ 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 diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index eb8be002..60697174 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -352,6 +352,8 @@ Сообщение удалено Выбрать почтовый ящик + Incognito mode is on + Thanks to incognito mode sender is not notified when you read the message Нет записей о замечаниях и свершениях Баллы @@ -738,6 +740,8 @@ Стоимость минуса Отвечать с историей сообщений Показывать среднее арифметическое при отсутствии стоимости + Incognito mode + Do not inform about reading the message Поддержка Политика приватности Соглашения diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index bcbd832a..20d8818b 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -37,7 +37,7 @@ Prihlásenie, číslo PESEL alebo e-mail Heslo Variácia denníka UONET+ - Custom domain suffix + Vlastná prípona domény Mobile API Scraper Hybridné @@ -352,6 +352,8 @@ 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 @@ -738,6 +740,8 @@ 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 diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index f89e38fc..db5c3cb0 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -352,6 +352,8 @@ Листи видалено Вибрати поштову скриньку + Режим анонімності включено + Завдяки режиму анонімності, відправник не буде сповіщений коли ви прочитаєте повідомлення Немає інформації о зауваженнях Бали @@ -738,6 +740,8 @@ Вартість мінуса Відповісти з історією повідомлень Вилічити середню аритметичну, якщо оцінка немає вартості + Анонімний режим + Не повідомляти про прочитання повідомлення Підтримка Політика конфіденційності Угоди From 722b4e58126de65c05dcee6c048cd410a7db6db0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Aug 2023 21:19:33 +0000 Subject: [PATCH 28/38] Bump androidx.preference:preference-ktx from 1.2.0 to 1.2.1 (#2274) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 197ea356..8ff34207 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -205,7 +205,7 @@ dependencies { implementation "androidx.fragment:fragment-ktx:1.6.1" implementation "androidx.annotation:annotation:1.6.0" - implementation "androidx.preference:preference-ktx:1.2.0" + implementation "androidx.preference:preference-ktx:1.2.1" implementation "androidx.recyclerview:recyclerview:1.3.1" implementation "androidx.viewpager2:viewpager2:1.1.0-beta02" implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" From c4396036ce7790fb4c35714393b6235be3c36512 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Aug 2023 21:19:49 +0000 Subject: [PATCH 29/38] Bump com.google.firebase:firebase-bom from 32.2.0 to 32.2.2 (#2271) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 8ff34207..5f148144 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -247,7 +247,7 @@ dependencies { implementation 'com.fredporciuncula:flow-preferences:1.9.1' implementation 'org.apache.commons:commons-text:1.10.0' - playImplementation platform('com.google.firebase:firebase-bom:32.2.0') + playImplementation platform('com.google.firebase:firebase-bom:32.2.2') playImplementation 'com.google.firebase:firebase-analytics-ktx' playImplementation 'com.google.firebase:firebase-messaging:' playImplementation 'com.google.firebase:firebase-crashlytics:' From e21c17ea99005a578d47f101b020ce1faa9b9985 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Aug 2023 21:20:05 +0000 Subject: [PATCH 30/38] Bump com.google.firebase:firebase-crashlytics-gradle from 2.9.7 to 2.9.8 (#2270) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index c5a6f598..2b52c068 100644 --- a/build.gradle +++ b/build.gradle @@ -18,7 +18,7 @@ buildscript { classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" classpath 'com.google.gms:google-services:4.3.15' classpath 'com.huawei.agconnect:agcp:1.9.1.300' - classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.7' + classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.8' classpath "com.github.triplet.gradle:play-publisher:3.8.4" classpath "ru.cian:huawei-publish-gradle-plugin:1.4.0" classpath "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:4.3.0.3225" From 8fbe341607467ea821e3b58b38193af2fe7fc639 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Aug 2023 21:20:19 +0000 Subject: [PATCH 31/38] Bump com.huawei.hms:hianalytics from 6.10.0.302 to 6.10.0.303 (#2272) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 5f148144..824c8376 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -256,7 +256,7 @@ dependencies { playImplementation 'com.google.android.play:core-ktx:1.8.1' playImplementation 'com.google.android.gms:play-services-ads:22.2.0' - hmsImplementation 'com.huawei.hms:hianalytics:6.10.0.302' + hmsImplementation 'com.huawei.hms:hianalytics:6.10.0.303' hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.9.1.300' releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker" From 7d5a29d4053fecdf7e1b1c6e4cf0d7cdf554789a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Aug 2023 21:20:45 +0000 Subject: [PATCH 32/38] Bump org.gradle.toolchains.foojay-resolver-convention (#2276) --- settings.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings.gradle b/settings.gradle index af9bb737..16731297 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,4 +1,4 @@ plugins { - id 'org.gradle.toolchains.foojay-resolver-convention' version '0.6.0' + id 'org.gradle.toolchains.foojay-resolver-convention' version '0.7.0' } include ':app' From 024ca897084bf1b939afb747ec4be1220d89ace8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Aug 2023 21:39:04 +0000 Subject: [PATCH 33/38] Bump mockk from 1.13.5 to 1.13.7 (#2275) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 824c8376..37b165c1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -186,7 +186,7 @@ ext { android_hilt = "1.0.0" room = "2.5.2" chucker = "3.5.2" - mockk = "1.13.5" + mockk = "1.13.7" coroutines = "1.7.3" } From 533157709b3db6ea03827b75660a1c77761cac7f Mon Sep 17 00:00:00 2001 From: Antoni Paduch <70513486+janAte1@users.noreply.github.com> Date: Tue, 22 Aug 2023 23:47:12 +0200 Subject: [PATCH 34/38] Add option to show empty tiles in the timetable (#2236) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mikołaj Pich --- app/src/main/assets/contributors.json | 4 + .../wulkanowy/data/enums/TimetableGapsMode.kt | 11 ++ .../repositories/PreferencesRepository.kt | 9 +- .../widgets/TimetableWidgetService.kt | 12 +- .../ui/modules/timetable/TimetableAdapter.kt | 47 ++++++-- .../ui/modules/timetable/TimetableItem.kt | 6 + .../modules/timetable/TimetablePresenter.kt | 71 +++++++++--- .../timetablewidget/TimetableWidgetFactory.kt | 107 ++++++++++++++---- .../timetablewidget/TimetableWidgetItem.kt | 26 +++++ .../main/res/layout/item_timetable_empty.xml | 43 +++++++ .../layout/item_widget_timetable_empty.xml | 36 ++++++ .../main/res/values-pl/preferences_values.xml | 5 + app/src/main/res/values-pl/strings.xml | 7 ++ .../main/res/values/preferences_defaults.xml | 1 + app/src/main/res/values/preferences_keys.xml | 1 + .../main/res/values/preferences_values.xml | 11 ++ app/src/main/res/values/strings.xml | 5 + .../res/xml/scheme_preferences_appearance.xml | 8 ++ 18 files changed, 363 insertions(+), 47 deletions(-) create mode 100644 app/src/main/java/io/github/wulkanowy/data/enums/TimetableGapsMode.kt create mode 100644 app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetItem.kt create mode 100644 app/src/main/res/layout/item_timetable_empty.xml create mode 100644 app/src/main/res/layout/item_widget_timetable_empty.xml diff --git a/app/src/main/assets/contributors.json b/app/src/main/assets/contributors.json index b2849931..a7629c22 100644 --- a/app/src/main/assets/contributors.json +++ b/app/src/main/assets/contributors.json @@ -50,5 +50,9 @@ { "displayName": "Tomasz F.", "githubUsername": "Pengwius" + }, + { + "displayName": "Antoni Paduch", + "githubUsername": "janAte1" } ] 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 00000000..c8310c02 --- /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/repositories/PreferencesRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt index 1b489340..85c74072 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 @@ -15,7 +15,6 @@ import io.github.wulkanowy.ui.modules.grade.GradeAverageMode 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 @@ -201,6 +200,14 @@ class PreferencesRepository @Inject constructor( 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 + ) + ) + val showSubjectsWithoutGrades: Boolean get() = getBoolean( R.string.pref_key_subjects_without_grades, 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 d48556fa..ffdb07ec 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 @@ -4,6 +4,7 @@ import android.content.Intent import android.widget.RemoteViewsService import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.data.db.SharedPrefProvider +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 @@ -26,10 +27,19 @@ class TimetableWidgetService : RemoteViewsService() { @Inject lateinit var sharedPref: SharedPrefProvider + @Inject + lateinit var prefRepository: PreferencesRepository + override fun onGetViewFactory(intent: Intent?): RemoteViewsFactory { Timber.d("TimetableWidgetFactory created") return TimetableWidgetFactory( - timetableRepo, studentRepo, semesterRepo, sharedPref, applicationContext, intent + 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/modules/timetable/TimetableAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableAdapter.kt index d917e7d5..1201937c 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 @@ -191,7 +218,8 @@ class TimetableAdapter @Inject constructor() : ) } else { timetableItemDescription.visibility = GONE - timetableItemRoom.isVisible = lesson.room.isNotBlank() || lesson.roomOld.isNotBlank() + timetableItemRoom.isVisible = + lesson.room.isNotBlank() || lesson.roomOld.isNotBlank() timetableItemGroup.isVisible = item.showGroupsInPlan && lesson.group.isNotBlank() timetableItemTeacher.visibility = VISIBLE } @@ -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 } 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 92716ace..105ece38 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 d0687408..0f8395de 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,44 @@ package io.github.wulkanowy.ui.modules.timetable -import io.github.wulkanowy.data.* 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.ui.base.BasePresenter import io.github.wulkanowy.ui.base.ErrorHandler -import io.github.wulkanowy.utils.* +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 kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.onEach import timber.log.Timber import java.time.Instant import java.time.LocalDate -import java.time.LocalDate.* -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 @@ -192,16 +213,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 + } } } 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 d545413d..4e0578e2 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 @@ -16,6 +16,9 @@ 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.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 @@ -24,6 +27,7 @@ import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetProvider.Co import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetProvider.Companion.getStudentWidgetKey import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetProvider.Companion.getTodayLastLessonEndDateTimeWidgetKey import io.github.wulkanowy.utils.getCompatColor +import io.github.wulkanowy.utils.getPlural import io.github.wulkanowy.utils.toFormattedString import kotlinx.coroutines.runBlocking import timber.log.Timber @@ -35,11 +39,12 @@ class TimetableWidgetFactory( private val studentRepository: StudentRepository, private val semesterRepository: SemesterRepository, 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 timetableCanceledColor: Int? = null @@ -47,18 +52,13 @@ class TimetableWidgetFactory( private var timetableChangeColor: Int? = null - private var lastSyncInstant: Instant? = null - override fun getLoadingView() = null override fun hasStableIds() = true - override fun getCount() = when { - lessons.isEmpty() -> 0 - else -> lessons.size + 1 - } + override fun getCount() = items.size - override fun getViewTypeCount() = 2 + override fun getViewTypeCount() = 3 override fun getItemId(position: Int) = position.toLong() @@ -75,9 +75,10 @@ class TimetableWidgetFactory( runBlocking { val student = getStudent(studentId) ?: return@runBlocking val semester = semesterRepository.getCurrentSemester(student) - lessons = getLessons(student, semester, date) - lastSyncInstant = - timetableRepository.getLastRefreshTimestamp(semester, date, date) + items = createItems( + lessons = getLessons(student, semester, date), + lastSync = timetableRepository.getLastRefreshTimestamp(semester, date, date) + ) if (date == LocalDate.now()) { updateTodayLastLessonEnd(appWidgetId) } @@ -101,8 +102,33 @@ class TimetableWidgetFactory( return lessons.sortedBy { it.number } } + 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 = lessons.maxOfOrNull { it.end } ?: return + val todayLastLessonEnd = items.filterIsInstance() + .maxOfOrNull { it.lesson.end } ?: return val key = getTodayLastLessonEndDateTimeWidgetKey(appWidgetId) sharedPref.putLong(key, todayLastLessonEnd.epochSecond, true) } @@ -112,15 +138,15 @@ class TimetableWidgetFactory( } override fun getViewAt(position: Int): RemoteViews? { - if (position == lessons.size) { - val synchronizationInstant = lastSyncInstant ?: Instant.MIN - val synchronizationText = getSynchronizationInfoText(synchronizationInstant) - return RemoteViews(context.packageName, R.layout.item_widget_timetable_footer).apply { - setTextViewText(R.id.timetableWidgetSynchronizationTime, synchronizationText) - } + return when (val item = items.getOrNull(position) ?: return null) { + is TimetableWidgetItem.Normal -> getNormalItemRemoteView(item) + is TimetableWidgetItem.Empty -> getEmptyItemRemoteView(item) + is TimetableWidgetItem.Synchronized -> getSynchronizedItemRemoteView(item) } + } - val lesson = lessons.getOrNull(position) ?: return null + 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) @@ -130,30 +156,63 @@ class TimetableWidgetFactory( 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, lesson + remoteViews = remoteViews, + lesson = lesson, ) } - return remoteViews } + 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 -> { 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 00000000..166b1a8f --- /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/res/layout/item_timetable_empty.xml b/app/src/main/res/layout/item_timetable_empty.xml new file mode 100644 index 00000000..12fddb75 --- /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_empty.xml b/app/src/main/res/layout/item_widget_timetable_empty.xml new file mode 100644 index 00000000..a48b3645 --- /dev/null +++ b/app/src/main/res/layout/item_widget_timetable_empty.xml @@ -0,0 +1,36 @@ + + + + + + + + diff --git a/app/src/main/res/values-pl/preferences_values.xml b/app/src/main/res/values-pl/preferences_values.xml index 45600574..8872b7ab 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 pokauj + 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 0c1bbf78..a2b5510e 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -185,6 +185,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 @@ -700,6 +706,7 @@ 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 diff --git a/app/src/main/res/values/preferences_defaults.xml b/app/src/main/res/values/preferences_defaults.xml index fefd9b13..8d69f25c 100644 --- a/app/src/main/res/values/preferences_defaults.xml +++ b/app/src/main/res/values/preferences_defaults.xml @@ -23,6 +23,7 @@ no alphabetic false + between false false 0 diff --git a/app/src/main/res/values/preferences_keys.xml b/app/src/main/res/values/preferences_keys.xml index e7fa542a..c48381e8 100644 --- a/app/src/main/res/values/preferences_keys.xml +++ b/app/src/main/res/values/preferences_keys.xml @@ -28,6 +28,7 @@ show_whole_class_plan show_groups_in_plan timetable_show_timers + timetable_show_gaps subjects_without_grades optional_arithmetic_average message_draft diff --git a/app/src/main/res/values/preferences_values.xml b/app/src/main/res/values/preferences_values.xml index 312f0b87..f56707c8 100644 --- a/app/src/main/res/values/preferences_values.xml +++ b/app/src/main/res/values/preferences_values.xml @@ -123,6 +123,17 @@ all_year + + Don\'t show + Only between lessons + Before and between lessons + + + no_gaps + between + before_and_between + + Lucky number Unread messages diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9dc7e796..ce277bdc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -186,6 +186,10 @@ Change of room from %1$s to %2$s Change of teacher from %1$s to %2$s Change of subject from %1$s to %2$s + + No lesson + No lessons + Timetable change Timetable changes @@ -690,6 +694,7 @@ Grades expanding Mark current lesson Show groups next to subjects + Show empty tiles where there\'s no lesson Show chart list in class grades Show subjects without grades Grades color scheme diff --git a/app/src/main/res/xml/scheme_preferences_appearance.xml b/app/src/main/res/xml/scheme_preferences_appearance.xml index 62216c76..7177d396 100644 --- a/app/src/main/res/xml/scheme_preferences_appearance.xml +++ b/app/src/main/res/xml/scheme_preferences_appearance.xml @@ -111,5 +111,13 @@ app:title="@string/pref_view_timetable_show_whole_class" app:useSimpleSummaryProvider="true" /> --> + From 2e2b13384a214fed9599a713fe7cbeedf0eb9cf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Wed, 23 Aug 2023 12:24:17 +0200 Subject: [PATCH 35/38] Try to switch to next school year before it starts (#2278) --- app/build.gradle | 2 +- .../data/repositories/SemesterRepository.kt | 4 +-- .../wulkanowy/utils/SemesterExtension.kt | 17 ++++++++-- .../repositories/SemesterRepositoryTest.kt | 23 +++++++------ .../utils/SemesterExtensionKtTest.kt | 34 +++++++++++++++++++ 5 files changed, 64 insertions(+), 16 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 37b165c1..136c5430 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -191,7 +191,7 @@ ext { } dependencies { - implementation 'io.github.wulkanowy:sdk:2.0.8' + implementation 'io.github.wulkanowy:sdk:2.0.9-SNAPSHOT' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3' 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 92bb3708..dd44df70 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 @@ -41,7 +41,7 @@ class SemesterRepository @Inject constructor( val isRefreshOnModeChangeRequired = when { Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE -> { - semesters.firstOrNull { it.isCurrent }?.let { + 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/utils/SemesterExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/SemesterExtension.kt index 380d6bf6..e3b8a3b4 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/test/java/io/github/wulkanowy/data/repositories/SemesterRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/SemesterRepositoryTest.kt index d8256869..31098d2e 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 @@ -81,15 +82,15 @@ class SemesterRepositoryTest { } @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) } 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 b7d3ecc9..e8ba8a87 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( From fbce9e58d034fe5616736d06bd4e5235f573d593 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Wed, 23 Aug 2023 19:46:53 +0200 Subject: [PATCH 36/38] New Crowdin updates (#2277) --- app/src/main/res/values-cs/preferences_values.xml | 5 +++++ app/src/main/res/values-cs/strings.xml | 7 +++++++ app/src/main/res/values-da-rDK/preferences_values.xml | 5 +++++ app/src/main/res/values-da-rDK/strings.xml | 5 +++++ app/src/main/res/values-de/preferences_values.xml | 5 +++++ app/src/main/res/values-de/strings.xml | 5 +++++ app/src/main/res/values-es-rES/preferences_values.xml | 5 +++++ app/src/main/res/values-es-rES/strings.xml | 5 +++++ app/src/main/res/values-it-rIT/preferences_values.xml | 5 +++++ app/src/main/res/values-it-rIT/strings.xml | 5 +++++ app/src/main/res/values-ru/preferences_values.xml | 5 +++++ app/src/main/res/values-ru/strings.xml | 7 +++++++ app/src/main/res/values-sk/preferences_values.xml | 5 +++++ app/src/main/res/values-sk/strings.xml | 7 +++++++ app/src/main/res/values-uk/preferences_values.xml | 5 +++++ app/src/main/res/values-uk/strings.xml | 7 +++++++ 16 files changed, 88 insertions(+) diff --git a/app/src/main/res/values-cs/preferences_values.xml b/app/src/main/res/values-cs/preferences_values.xml index 23073adf..2cf40263 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 + + Don\'t show + Only between lessons + Before and between lessons + Šť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 ff461d42..a63a0aa1 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -185,6 +185,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 + + No lesson + No lessons + No lessons + No lessons + Změna plánu lekcí Změny plánu lekcí @@ -700,6 +706,7 @@ Rozvíjení známek Označit aktuální lekci Zobrazit skupiny vedle předmětů + Show empty tiles where there\'s no lesson Zobrazit seznam grafů v známkách třídy Zobrazit předměty bez známek Známky barevné schéma diff --git a/app/src/main/res/values-da-rDK/preferences_values.xml b/app/src/main/res/values-da-rDK/preferences_values.xml index ac2b6e9e..5aff12de 100644 --- a/app/src/main/res/values-da-rDK/preferences_values.xml +++ b/app/src/main/res/values-da-rDK/preferences_values.xml @@ -51,6 +51,11 @@ 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 diff --git a/app/src/main/res/values-da-rDK/strings.xml b/app/src/main/res/values-da-rDK/strings.xml index 259a4264..2abf1a4a 100644 --- a/app/src/main/res/values-da-rDK/strings.xml +++ b/app/src/main/res/values-da-rDK/strings.xml @@ -171,6 +171,10 @@ Change of room from %1$s to %2$s Change of teacher from %1$s to %2$s Change of subject from %1$s to %2$s + + No lesson + No lessons + Timetable change Timetable changes @@ -612,6 +616,7 @@ Grades expanding Mark current lesson Show groups next to subjects + Show empty tiles where there\'s no lesson Show chart list in class grades Show subjects without grades Grades color scheme diff --git a/app/src/main/res/values-de/preferences_values.xml b/app/src/main/res/values-de/preferences_values.xml index d9cac195..d1001c74 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 1836d047..f08a504a 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -171,6 +171,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 @@ -612,6 +616,7 @@ 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 diff --git a/app/src/main/res/values-es-rES/preferences_values.xml b/app/src/main/res/values-es-rES/preferences_values.xml index ac2b6e9e..5aff12de 100644 --- a/app/src/main/res/values-es-rES/preferences_values.xml +++ b/app/src/main/res/values-es-rES/preferences_values.xml @@ -51,6 +51,11 @@ 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 diff --git a/app/src/main/res/values-es-rES/strings.xml b/app/src/main/res/values-es-rES/strings.xml index 259a4264..2abf1a4a 100644 --- a/app/src/main/res/values-es-rES/strings.xml +++ b/app/src/main/res/values-es-rES/strings.xml @@ -171,6 +171,10 @@ Change of room from %1$s to %2$s Change of teacher from %1$s to %2$s Change of subject from %1$s to %2$s + + No lesson + No lessons + Timetable change Timetable changes @@ -612,6 +616,7 @@ Grades expanding Mark current lesson Show groups next to subjects + Show empty tiles where there\'s no lesson Show chart list in class grades Show subjects without grades Grades color scheme diff --git a/app/src/main/res/values-it-rIT/preferences_values.xml b/app/src/main/res/values-it-rIT/preferences_values.xml index ac2b6e9e..5aff12de 100644 --- a/app/src/main/res/values-it-rIT/preferences_values.xml +++ b/app/src/main/res/values-it-rIT/preferences_values.xml @@ -51,6 +51,11 @@ 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 diff --git a/app/src/main/res/values-it-rIT/strings.xml b/app/src/main/res/values-it-rIT/strings.xml index 259a4264..2abf1a4a 100644 --- a/app/src/main/res/values-it-rIT/strings.xml +++ b/app/src/main/res/values-it-rIT/strings.xml @@ -171,6 +171,10 @@ Change of room from %1$s to %2$s Change of teacher from %1$s to %2$s Change of subject from %1$s to %2$s + + No lesson + No lessons + Timetable change Timetable changes @@ -612,6 +616,7 @@ Grades expanding Mark current lesson Show groups next to subjects + Show empty tiles where there\'s no lesson Show chart list in class grades Show subjects without grades Grades color scheme diff --git a/app/src/main/res/values-ru/preferences_values.xml b/app/src/main/res/values-ru/preferences_values.xml index 8a8c260d..df3629c0 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 60697174..d075dac6 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -185,6 +185,12 @@ Аудитория изменена с %1$s на %2$s Учитель изменён с %1$s на %2$s Тема изменена с %1$s на %2$s + + No lesson + No lessons + No lessons + No lessons + Изменение расписания Изменения расписания @@ -700,6 +706,7 @@ Разворачивание оценок Отметить текущий урок Показать группы рядом с темами + Show empty tiles where there\'s no lesson Показывать диаграммы в оценках класса Показать предметы без оценок Цветовая схема оценок diff --git a/app/src/main/res/values-sk/preferences_values.xml b/app/src/main/res/values-sk/preferences_values.xml index e4331315..fd393394 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 + + Don\'t show + Only between lessons + Before and between lessons + Šť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 20d8818b..d1990e35 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -185,6 +185,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 + + No lesson + No lessons + No lessons + No lessons + Zmena plánu lekcií Zmeny plánu lekcií @@ -700,6 +706,7 @@ Rozvijanie známok Označiť aktuálne lekciu Zobraziť skupiny vedľa predmetov + Show empty tiles where there\'s no lesson Zobraziť zoznam grafov v známkach triedy Zobraziť predmety bez známok Známky farebnú schému diff --git a/app/src/main/res/values-uk/preferences_values.xml b/app/src/main/res/values-uk/preferences_values.xml index 44acd18e..55cf905b 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 @@ Середнє значення з обох семестрів Середня оцінка з цілого року + + Don\'t show + Only between lessons + Before and between lessons + Щасливий номер Непрочитані листи diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index db5c3cb0..876562d3 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -185,6 +185,12 @@ Зміна аудіторії з %1$s на %2$s Зміна вчителя з %1$s на %2$s Зміна теми з %1$s на %2$s + + No lesson + No lessons + No lessons + No lessons + Зміна у розкладі Зміни у розкладі @@ -700,6 +706,7 @@ Розгортання оцінок Позначити поточний урок Показувати групи поруч з темами + Show empty tiles where there\'s no lesson Показувати діаграми в оцінках класу Показати предмети без оцінок Схема кольорів оцінок From 3dfc55c4d196cb767704e8f6f73ac1e25f611df8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Thu, 24 Aug 2023 11:33:40 +0200 Subject: [PATCH 37/38] Add admin messages to login screen (#2280) --- app/build.gradle | 2 +- .../57.json | 2443 +++++++++++++++++ .../java/io/github/wulkanowy/data/Resource.kt | 2 +- .../github/wulkanowy/data/db/AppDatabase.kt | 3 +- .../io/github/wulkanowy/data/db/Converters.kt | 6 + .../data/db/entities/AdminMessage.kt | 4 +- .../data/db/migrations/Migration57.kt | 10 + .../wulkanowy/data/enums/MessageType.kt | 9 + .../repositories/AdminMessageRepository.kt | 45 +- .../GetAppropriateAdminMessageUseCase.kt | 64 + .../dashboard/DashboardItemMoveCallback.kt | 3 +- .../modules/dashboard/DashboardPresenter.kt | 34 +- .../dashboard/adapters/DashboardAdapter.kt | 45 +- .../viewholders/AdminMessageViewHolder.kt | 52 + .../modules/login/form/LoginFormFragment.kt | 15 + .../modules/login/form/LoginFormPresenter.kt | 40 +- .../ui/modules/login/form/LoginFormView.kt | 5 + .../main/res/layout/fragment_login_form.xml | 15 +- .../login/form/LoginFormPresenterTest.kt | 10 + 19 files changed, 2722 insertions(+), 85 deletions(-) create mode 100644 app/schemas/io.github.wulkanowy.data.db.AppDatabase/57.json create mode 100644 app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration57.kt create mode 100644 app/src/main/java/io/github/wulkanowy/data/enums/MessageType.kt create mode 100644 app/src/main/java/io/github/wulkanowy/domain/adminmessage/GetAppropriateAdminMessageUseCase.kt create mode 100644 app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/viewholders/AdminMessageViewHolder.kt diff --git a/app/build.gradle b/app/build.gradle index 136c5430..d0ae8894 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -27,7 +27,7 @@ android { testApplicationId "io.github.tests.wulkanowy" minSdkVersion 21 targetSdkVersion 33 - versionCode 130 + versionCode 131 // todo: already bumped for 2.1.0 version versionName "2.0.8" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 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 00000000..2eff1223 --- /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/src/main/java/io/github/wulkanowy/data/Resource.kt b/app/src/main/java/io/github/wulkanowy/data/Resource.kt index 6b611e47..2c5bf0ea 100644 --- a/app/src/main/java/io/github/wulkanowy/data/Resource.kt +++ b/app/src/main/java/io/github/wulkanowy/data/Resource.kt @@ -148,7 +148,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()) 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 882a7016..48a2942c 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 @@ -50,6 +50,7 @@ import javax.inject.Singleton 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), ], version = AppDatabase.VERSION_SCHEMA, exportSchema = true @@ -58,7 +59,7 @@ import javax.inject.Singleton abstract class AppDatabase : RoomDatabase() { companion object { - const val VERSION_SCHEMA = 56 + const val VERSION_SCHEMA = 57 fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf( Migration2(), 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 9d3beae1..7bc8d12a 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/entities/AdminMessage.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/AdminMessage.kt index 97fec69b..875c2a3a 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,7 +34,8 @@ 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 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 00000000..2fc8718f --- /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/enums/MessageType.kt b/app/src/main/java/io/github/wulkanowy/data/enums/MessageType.kt new file mode 100644 index 00000000..531684e4 --- /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/repositories/AdminMessageRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/AdminMessageRepository.kt index c9655b72..b831ee75 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/domain/adminmessage/GetAppropriateAdminMessageUseCase.kt b/app/src/main/java/io/github/wulkanowy/domain/adminmessage/GetAppropriateAdminMessageUseCase.kt new file mode 100644 index 00000000..b55bf899 --- /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/ui/modules/dashboard/DashboardItemMoveCallback.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardItemMoveCallback.kt index 9c15acc3..f033b594 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 ac2c896d..ecf084c6 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt @@ -5,7 +5,9 @@ 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.enums.MessageType import io.github.wulkanowy.data.repositories.* +import io.github.wulkanowy.domain.adminmessage.GetAppropriateAdminMessageUseCase import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.utils.AdsHelper @@ -32,7 +34,7 @@ class DashboardPresenter @Inject constructor( 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) { @@ -159,19 +161,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) } @@ -355,6 +361,7 @@ class DashboardPresenter @Inject constructor( firstLoadedItemList += DashboardItem.Type.GRADES } } + is Resource.Success -> { Timber.i("Loading dashboard grades result: Success") updateData( @@ -365,6 +372,7 @@ class DashboardPresenter @Inject constructor( forceRefresh ) } + is Resource.Error -> { Timber.i("Loading dashboard grades result: An exception occurred") errorHandler.dispatch(it.error) @@ -402,12 +410,14 @@ class DashboardPresenter @Inject constructor( 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) @@ -457,10 +467,12 @@ class DashboardPresenter @Inject constructor( 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) @@ -489,10 +501,12 @@ class DashboardPresenter @Inject constructor( 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) @@ -530,10 +544,12 @@ class DashboardPresenter @Inject constructor( 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) @@ -569,10 +585,12 @@ class DashboardPresenter @Inject constructor( 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) @@ -584,12 +602,12 @@ 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 -> { @@ -597,6 +615,7 @@ class DashboardPresenter @Inject constructor( if (forceRefresh) return@onEach updateData(DashboardItem.AdminMessages(), forceRefresh) } + is Resource.Success -> { Timber.i("Loading dashboard admin message result: Success") updateData( @@ -604,6 +623,7 @@ class DashboardPresenter @Inject constructor( forceRefresh = forceRefresh ) } + is Resource.Error -> { Timber.i("Loading dashboard admin message result: An exception occurred") Timber.e(it.error) 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 4ad4e9d6..7c74cae8 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) } } @@ -733,39 +734,6 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter { - 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(adminMessageViewHolder.binding) { - dashboardAdminMessageItemTitle.text = item.title - dashboardAdminMessageItemTitle.setTextColor(textColor) - dashboardAdminMessageItemDescription.text = item.content - dashboardAdminMessageItemDescription.setTextColor(textColor) - dashboardAdminMessageItemIcon.setColorFilter(textColor) - dashboardAdminMessageItemDismiss.isVisible = item.isDismissible - dashboardAdminMessageItemDismiss.setTextColor(textColor) - 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 @@ -819,9 +787,6 @@ class DashboardAdapter @Inject constructor() : 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.isDismissible + dashboardAdminMessageItemDismiss.setTextColor(textColor) + dashboardAdminMessageItemDismiss.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/login/form/LoginFormFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt index 1085ff50..ff7fd864 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 @@ -9,10 +9,12 @@ 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.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.dashboard.viewholders.AdminMessageViewHolder import io.github.wulkanowy.ui.modules.login.LoginActivity import io.github.wulkanowy.ui.modules.login.LoginData import io.github.wulkanowy.utils.* @@ -207,6 +209,19 @@ 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 } 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 85f42841..4e0404d9 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,13 +1,19 @@ package io.github.wulkanowy.ui.modules.login.form import androidx.core.net.toUri +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 @@ -22,7 +28,9 @@ class LoginFormPresenter @Inject constructor( studentRepository: StudentRepository, private val loginErrorHandler: LoginErrorHandler, private val appInfo: AppInfo, - private val analytics: AnalyticsHelper + private val analytics: AnalyticsHelper, + private val getAppropriateAdminMessageUseCase: GetAppropriateAdminMessageUseCase, + private val preferencesRepository: PreferencesRepository, ) : BasePresenter(loginErrorHandler, studentRepository) { private var lastError: Throwable? = null @@ -41,6 +49,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() { @@ -63,6 +96,7 @@ class LoginFormPresenter @Inject constructor( } updateCustomDomainSuffixVisibility() updateUsernameLabel() + reloadAdminMessage() } } @@ -103,7 +137,9 @@ class LoginFormPresenter @Inject constructor( val email = view?.formUsernameValue.orEmpty().trim() val password = view?.formPassValue.orEmpty().trim() val host = view?.formHostValue.orEmpty().trim() - val domainSuffix = view?.formDomainSuffix.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 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 5fb26062..5b4dcadf 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,5 +1,6 @@ package io.github.wulkanowy.ui.modules.login.form +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 @@ -58,6 +59,10 @@ interface LoginFormView : BaseView { fun showContent(show: Boolean) + fun showAdminMessage(message: AdminMessage?) + + fun openInternetBrowser(url: String) + fun showDomainSuffixInput(show: Boolean) fun showOtherOptionsButton(show: Boolean) diff --git a/app/src/main/res/layout/fragment_login_form.xml b/app/src/main/res/layout/fragment_login_form.xml index 39f3146c..fc5e5f35 100644 --- a/app/src/main/res/layout/fragment_login_form.xml +++ b/app/src/main/res/layout/fragment_login_form.xml @@ -105,6 +105,18 @@ android:background="?android:attr/listDivider" /> + + Date: Fri, 25 Aug 2023 00:01:36 +0200 Subject: [PATCH 38/38] Version 2.1.0 --- app/build.gradle | 8 ++++---- app/src/main/play/release-notes/pl-PL/default.txt | 9 +++++---- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index d0ae8894..b09078a9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -27,8 +27,8 @@ android { testApplicationId "io.github.tests.wulkanowy" minSdkVersion 21 targetSdkVersion 33 - versionCode 131 // todo: already bumped for 2.1.0 version - versionName "2.0.8" + versionCode 131 + versionName "2.1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" resValue "string", "app_name", "Wulkanowy" @@ -161,7 +161,7 @@ play { track = 'production' releaseStatus = ReleaseStatus.IN_PROGRESS userFraction = 0.25d - updatePriority = 1 + updatePriority = 3 enabled.set(false) } @@ -191,7 +191,7 @@ ext { } dependencies { - implementation 'io.github.wulkanowy:sdk:2.0.9-SNAPSHOT' + implementation 'io.github.wulkanowy:sdk:2.1.0' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3' 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 e881cfda..aa934ce9 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,8 @@ -Wersja 2.0.8 +Wersja 2.1.0 -— poprawiliśmy wyświetlanie kilku rodzajów zmian w planie lekcji -— dodaliśmy limit znaków w okienku usprawiedliwiania -— naprawiliśmy wyświetlanie frekwencji w szkołach, gdzie działa już system eduOne (ciągle jednak brak opcji usprawiedliwiania) +— dodaliśmy tryb incognito w wiadomościach +— dodaliśmy wyświetlanie pustych lekcji (okienek) w planie lekcji +— poprawiliśmy widżet planu lekcji (będzie teraz trochę bardziej kompaktowy) +— zmieniliśmy datę rozpoczęcia roku szkolnego na 3 dni przed 1 września (sorry) Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases