Compare commits

...

124 Commits
2.0.1 ... 2.2.0

Author SHA1 Message Date
1d8378e136 Merge branch 'release/2.2.0' 2023-09-26 23:13:40 +02:00
4a2bf539f0 Version 2.2.0 2023-09-26 23:13:32 +02:00
4d085f8266 New Crowdin updates (#2311) 2023-09-26 21:04:27 +00:00
fca69e7234 Add form dialog to login e-mail support (#2306) 2023-09-26 22:27:08 +02:00
711de0f77f Fix average calculation when there is no real semesters available (#2310) 2023-09-26 22:26:19 +02:00
58d5196ac9 Improve symbol input field (#2312) 2023-09-26 22:25:23 +02:00
26a95ecb99 Auto select students for login (#2307)
* Auto select students for login

* Add sign in icon to sign in button on student select screen
2023-09-26 21:02:36 +02:00
1835446468 Fix password reset related issues (#2308)
* Fix login hint in password reset field

* Don't hide first password reset button

* Change recover button label
2023-09-26 21:01:59 +02:00
4d3b16ec80 Fix password toggle icon tint after clearing error (#2309) 2023-09-26 21:01:25 +02:00
95b4d53fac Add schools API integration (#2302) 2023-09-25 19:44:13 +02:00
0fa197d520 New Crowdin updates (#2303) 2023-09-25 19:43:57 +02:00
646b4a149d Bump about_libraries from 10.8.3 to 10.9.0 (#2304) 2023-09-25 17:43:41 +00:00
afd0c8513a Bump mockk from 1.13.7 to 1.13.8 (#2305) 2023-09-25 17:43:16 +00:00
c4a3da93ca Bump com.huawei.hms:hianalytics from 6.10.0.303 to 6.12.0.300 (#2294) 2023-09-20 21:00:06 +00:00
ff2aa6f195 Bump com.huawei.agconnect:agcp from 1.9.1.300 to 1.9.1.301 (#2297) 2023-09-20 20:59:38 +00:00
1d8d71709f Bump com.huawei.agconnect:agconnect-crash from 1.9.1.300 to 1.9.1.301 (#2298) 2023-09-20 20:49:46 +00:00
aabd7345c1 Bump com.google.firebase:firebase-bom from 32.2.3 to 32.3.1 (#2299) 2023-09-20 20:49:25 +00:00
09d16cf6d8 Bump androidx.annotation:annotation from 1.6.0 to 1.7.0 (#2293) 2023-09-20 20:33:50 +00:00
81d8f7ea48 Bump androidx.lifecycle:lifecycle-livedata-ktx from 2.6.1 to 2.6.2 (#2295) 2023-09-20 20:33:30 +00:00
05a804832b Bump com.google.gms:google-services from 4.3.15 to 4.4.0 (#2300) 2023-09-20 20:33:10 +00:00
db02f0c1e1 Bump com.google.android.gms:play-services-ads from 22.3.0 to 22.4.0 (#2301) 2023-09-20 20:32:56 +00:00
0a40237809 Bump com.github.bastienpaulfr:Treessence from 1.0.5 to 1.1.2 (#2289) 2023-09-04 18:30:02 +00:00
017d46e5db Bump hilt_version from 2.47 to 2.48 (#2290) 2023-09-04 18:13:39 +00:00
8478b8b7ed Bump org.sonarsource.scanner.gradle:sonarqube-gradle-plugin (#2291) 2023-09-04 18:13:24 +00:00
8cc69728aa Bump kotlin_version from 1.9.0 to 1.9.10 (#2281) 2023-09-01 17:29:02 +00:00
c82e6ae95b New Crowdin updates (#2287) 2023-09-01 19:06:48 +02:00
50a177d18c Bump org.jetbrains.kotlinx:kotlinx-serialization-json (#2282) 2023-09-01 17:05:47 +00:00
a77b3d4cd7 Bump com.google.firebase:firebase-crashlytics-gradle from 2.9.8 to 2.9.9 (#2283) 2023-09-01 17:05:29 +00:00
aff56a8311 Bump com.google.firebase:firebase-bom from 32.2.2 to 32.2.3 (#2284) 2023-09-01 17:05:12 +00:00
5238e4d187 Bump com.google.android.gms:play-services-ads from 22.2.0 to 22.3.0 (#2285) 2023-09-01 17:04:57 +00:00
10f9812495 Bump com.android.tools.build:gradle from 8.1.0 to 8.1.1 (#2286) 2023-09-01 17:04:34 +00:00
ab1de323d4 Merge branch 'release/2.1.0' into develop 2023-08-25 00:01:47 +02:00
af346842a3 Merge branch 'release/2.1.0' 2023-08-25 00:01:42 +02:00
8f78324940 Version 2.1.0 2023-08-25 00:01:36 +02:00
3dfc55c4d1 Add admin messages to login screen (#2280) 2023-08-24 11:33:40 +02:00
fbce9e58d0 New Crowdin updates (#2277) 2023-08-23 19:46:53 +02:00
2e2b13384a Try to switch to next school year before it starts (#2278) 2023-08-23 12:24:17 +02:00
533157709b Add option to show empty tiles in the timetable (#2236)
Co-authored-by: Mikołaj Pich <m.pich@outlook.com>
2023-08-22 23:47:12 +02:00
024ca89708 Bump mockk from 1.13.5 to 1.13.7 (#2275) 2023-08-22 21:39:04 +00:00
7d5a29d405 Bump org.gradle.toolchains.foojay-resolver-convention (#2276) 2023-08-22 21:20:45 +00:00
8fbe341607 Bump com.huawei.hms:hianalytics from 6.10.0.302 to 6.10.0.303 (#2272) 2023-08-22 21:20:19 +00:00
e21c17ea99 Bump com.google.firebase:firebase-crashlytics-gradle from 2.9.7 to 2.9.8 (#2270) 2023-08-22 21:20:05 +00:00
c4396036ce Bump com.google.firebase:firebase-bom from 32.2.0 to 32.2.2 (#2271) 2023-08-22 21:19:49 +00:00
722b4e5812 Bump androidx.preference:preference-ktx from 1.2.0 to 1.2.1 (#2274) 2023-08-22 21:19:33 +00:00
74820f9571 New Crowdin updates (#2265) 2023-07-31 21:32:07 +02:00
50326c7a48 Bump androidx.recyclerview:recyclerview from 1.3.0 to 1.3.1 (#2268) 2023-07-31 19:00:37 +00:00
0f129109ba Bump com.android.tools.build:gradle from 8.0.2 to 8.1.0 (#2266) 2023-07-31 19:00:17 +00:00
fc2adff997 Bump androidx.fragment:fragment-ktx from 1.6.0 to 1.6.1 (#2269) 2023-07-31 17:35:38 +00:00
7f6a13a9ee Bump coroutines from 1.7.2 to 1.7.3 (#2267) 2023-07-31 17:35:29 +00:00
64cc24ae60 Add incognito mode in messages (#1970)
Co-authored-by: Mikołaj Pich <m.pich@outlook.com>
2023-07-26 22:17:58 +02:00
91d7ee442e New Crowdin updates (#2257) 2023-07-26 19:37:06 +02:00
b296926423 Timetable widget improvements (#2219) 2023-07-25 21:05:14 +00:00
398bc513fb Bump about_libraries from 10.8.0 to 10.8.3 (#2263) 2023-07-25 10:00:31 +00:00
5b2e2ffb34 Remove tests deprecations (#2260) 2023-07-25 11:37:43 +02:00
e79c5d4d2b Bump hilt_version from 2.46.1 to 2.47 (#2261) 2023-07-24 21:36:06 +00:00
c0161f38c6 Bump org.sonarsource.scanner.gradle:sonarqube-gradle-plugin (#2262) 2023-07-24 21:33:17 +00:00
ef72218906 Bump org.gradle.toolchains.foojay-resolver-convention (#2264) 2023-07-24 21:30:37 +00:00
05741761a2 Bump kotlin_version from 1.8.22 to 1.9.0 (#2255) 2023-07-17 20:08:56 +00:00
86c7de6595 Bump room from 2.5.1 to 2.5.2 (#2250) 2023-07-17 19:15:48 +00:00
88ea753fc6 Bump coroutines from 1.7.1 to 1.7.2 (#2251) 2023-07-17 19:15:34 +00:00
d0819928f3 Bump com.huawei.agconnect:agconnect-crash from 1.9.0.300 to 1.9.1.300 (#2252) 2023-07-17 19:15:21 +00:00
8564e12b01 Bump com.huawei.agconnect:agcp from 1.9.0.300 to 1.9.1.300 (#2254) 2023-07-17 19:15:06 +00:00
29a36aaf6e Bump com.huawei.hms:hianalytics from 6.10.0.301 to 6.10.0.302 (#2253) 2023-07-17 18:11:57 +00:00
dbe608f2dd Bump about_libraries from 10.7.0 to 10.8.0 (#2249) 2023-07-17 17:54:26 +00:00
bb79b33b6d Bump com.google.android.gms:play-services-ads from 22.1.0 to 22.2.0 (#2256) 2023-07-17 17:53:58 +00:00
6e7c12a118 Bump com.google.firebase:firebase-crashlytics-gradle from 2.9.5 to 2.9.7 (#2258) 2023-07-17 17:53:38 +00:00
03cd3aeab7 Bump com.google.firebase:firebase-bom from 32.1.0 to 32.2.0 (#2259) 2023-07-17 17:53:21 +00:00
df8849639b Bump org.sonarsource.scanner.gradle:sonarqube-gradle-plugin (#2242) 2023-06-14 10:56:32 +00:00
8913b22a20 Bump org.sonarsource.scanner.gradle:sonarqube-gradle-plugin (#2237) 2023-06-08 23:25:46 +00:00
f20ffe44d5 Bump kotlin_version from 1.8.21 to 1.8.22 (#2238) 2023-06-08 23:24:27 +00:00
2f749a690b Bump androidx.fragment:fragment-ktx from 1.5.7 to 1.6.0 (#2239) 2023-06-08 23:18:23 +00:00
ae1951bf58 Merge branch 'release/2.0.8' into develop 2023-06-01 23:43:12 +02:00
391f38485d Merge branch 'release/2.0.8' 2023-06-01 23:43:06 +02:00
fecd5c707d Version 2.0.8 2023-06-01 23:43:01 +02:00
fa44295d59 New Crowdin updates (#2231) 2023-06-01 23:37:01 +02:00
5306044173 Add custom register host field on login screen (#2221) 2023-06-01 23:22:10 +02:00
c1b86674c2 Merge branch 'release/2.0.7' into develop 2023-06-01 11:00:01 +02:00
fd482777e8 Merge branch 'release/2.0.7' 2023-06-01 10:59:55 +02:00
d4ae0d56d6 Version 2.0.7 2023-06-01 10:59:50 +02:00
63487249b8 New Crowdin updates (#2211) 2023-06-01 10:31:42 +02:00
1bc0f2d214 Add character limit to attendance excuse content (#2222) 2023-06-01 10:30:50 +02:00
41bde45731 Bump androidx.viewpager2:viewpager2 from 1.1.0-beta01 to 1.1.0-beta02 (#2227) 2023-05-31 15:19:25 +00:00
556f42195b Bump com.android.tools.build:gradle from 8.0.1 to 8.0.2 (#2228) 2023-05-31 15:18:55 +00:00
06fd7b0c36 Bump com.google.firebase:firebase-bom from 32.0.0 to 32.1.0 (#2225) 2023-05-31 15:18:30 +00:00
db4e4d8cef Bump com.huawei.hms:hianalytics from 6.10.0.300 to 6.10.0.301 (#2224) 2023-05-31 15:18:07 +00:00
48e4a9fec5 Bump org.sonarsource.scanner.gradle:sonarqube-gradle-plugin (#2223) 2023-05-31 15:17:48 +00:00
70333737cf Bump androidx.activity:activity-ktx from 1.7.1 to 1.7.2 (#2226) 2023-05-31 15:16:42 +00:00
3096fa1538 Bump about_libraries from 10.6.3 to 10.7.0 (#2214) 2023-05-24 21:50:17 +00:00
19ed121466 Bump io.coil-kt:coil from 2.3.0 to 2.4.0 (#2215) 2023-05-24 21:32:28 +00:00
e7733bfa2a Bump com.google.android.gms:play-services-ads from 22.0.0 to 22.1.0 (#2216) 2023-05-24 21:31:38 +00:00
b9b464ea9b Merge branch 'release/2.0.6' into develop 2023-05-23 16:26:43 +02:00
cc46b3b124 Merge branch 'release/2.0.6' 2023-05-23 16:26:36 +02:00
092e86b621 Version 2.0.6 2023-05-23 16:26:31 +02:00
c170614461 Add R8 rule for Wulkanowy SDK (#2220) 2023-05-23 14:09:48 +02:00
6ce8e00ebf Merge branch 'release/2.0.5' into develop 2023-05-23 02:37:55 +02:00
c40cdf88ad Merge branch 'release/2.0.5' 2023-05-23 02:37:43 +02:00
4c1fe233c7 Version 2.0.5 2023-05-23 02:37:38 +02:00
aca88b57e0 Add r8 rules for HMS SDK (#2217) 2023-05-23 02:16:42 +02:00
a603c12625 Merge branch 'release/2.0.4' into develop 2023-05-22 17:10:09 +02:00
5c440010e2 Merge branch 'release/2.0.4' 2023-05-22 17:10:02 +02:00
4920317573 Version 2.0.4 2023-05-22 17:09:57 +02:00
9466482893 Add foojay-resolver and update dependencies (#2212) 2023-05-18 14:14:14 +00:00
48bcf581cf Bump org.jetbrains.kotlinx:kotlinx-serialization-json (#2209) 2023-05-15 18:36:50 +00:00
8a7b7103eb Bump coroutines from 1.7.0 to 1.7.1 (#2207) 2023-05-15 18:36:26 +00:00
ea312c3e12 Bump androidx.core:core-ktx from 1.10.0 to 1.10.1 (#2208) 2023-05-15 17:13:02 +00:00
5b0fe2c006 Bump ru.cian:huawei-publish-gradle-plugin from 1.3.5 to 1.4.0 (#2185) 2023-05-14 19:28:49 +00:00
a06add070e Bump com.android.tools.build:gradle from 7.4.2 to 8.0.1 (#2187) 2023-05-14 18:20:56 +00:00
dce491bffe Bump hilt_version from 2.45 to 2.46.1 (#2205) 2023-05-14 17:28:29 +00:00
adf418cc68 Fix delete user homework button visibility (#2204) 2023-05-13 10:44:09 +02:00
defcfec971 Merge branch 'release/2.0.3' into develop 2023-05-12 22:59:59 +02:00
d08f195968 Merge branch 'release/2.0.3' 2023-05-12 22:59:45 +02:00
1e9a6a5c42 Version 2.0.3 2023-05-12 22:59:40 +02:00
cc752ab0ad New Crowdin updates (#2201) 2023-05-12 22:45:50 +02:00
f2faa7e8b7 Fix button color in high priority admin message (#2202) 2023-05-12 22:45:24 +02:00
030fe8c218 Bump coroutines from 1.6.4 to 1.7.0 (#2186) 2023-05-12 04:45:15 +00:00
c33b309cf0 Bump org.robolectric:robolectric from 4.10 to 4.10.2 (#2188) 2023-05-12 04:34:48 +00:00
a0af55825d Bump about_libraries from 10.6.2 to 10.6.3 (#2189) 2023-05-12 04:34:32 +00:00
cb8303f33d Bump com.google.android.material:material from 1.8.0 to 1.9.0 (#2191) 2023-05-12 04:34:14 +00:00
54fbd56b73 Bump com.google.firebase:firebase-bom from 31.5.0 to 32.0.0 (#2190) 2023-05-12 04:33:56 +00:00
70f50cd51b Merge branch 'release/2.0.2' into develop 2023-05-12 00:46:02 +02:00
88c38c4a8d Merge branch 'release/2.0.2' 2023-05-12 00:45:55 +02:00
b8ac72c247 Version 2.0.2 2023-05-12 00:45:48 +02:00
cbef160ada New Crowdin updates (#2193) 2023-05-12 00:41:53 +02:00
e77894bf3e Merge branch 'release/2.0.1' into develop 2023-05-12 00:21:36 +02:00
146 changed files with 8209 additions and 694 deletions

View File

@ -16,7 +16,7 @@ jobs:
- uses: actions/setup-java@v2 - uses: actions/setup-java@v2
with: with:
distribution: 'zulu' distribution: 'zulu'
java-version: 11 java-version: 17
- uses: actions/cache@v3 - uses: actions/cache@v3
with: with:
path: | path: |
@ -52,7 +52,7 @@ jobs:
- uses: actions/setup-java@v2 - uses: actions/setup-java@v2
with: with:
distribution: 'zulu' distribution: 'zulu'
java-version: 11 java-version: 17
- uses: actions/cache@v3 - uses: actions/cache@v3
with: with:
path: | path: |

View File

@ -22,7 +22,7 @@ jobs:
- uses: actions/setup-java@v2 - uses: actions/setup-java@v2
with: with:
distribution: 'zulu' distribution: 'zulu'
java-version: 11 java-version: 17
- uses: actions/cache@v3 - uses: actions/cache@v3
with: with:
path: | path: |
@ -92,7 +92,7 @@ jobs:
- uses: actions/setup-java@v2 - uses: actions/setup-java@v2
with: with:
distribution: 'zulu' distribution: 'zulu'
java-version: 11 java-version: 17
- uses: actions/cache@v3 - uses: actions/cache@v3
with: with:
path: | path: |

View File

@ -22,7 +22,7 @@ jobs:
- uses: actions/setup-java@v2 - uses: actions/setup-java@v2
with: with:
distribution: 'zulu' distribution: 'zulu'
java-version: 11 java-version: 17
- uses: actions/cache@v3 - uses: actions/cache@v3
with: with:
path: | path: |
@ -48,7 +48,7 @@ jobs:
- uses: actions/setup-java@v2 - uses: actions/setup-java@v2
with: with:
distribution: 'zulu' distribution: 'zulu'
java-version: 11 java-version: 17
- uses: actions/cache@v3 - uses: actions/cache@v3
with: with:
path: | path: |
@ -74,7 +74,7 @@ jobs:
- uses: actions/setup-java@v2 - uses: actions/setup-java@v2
with: with:
distribution: 'zulu' distribution: 'zulu'
java-version: 11 java-version: 17
- uses: actions/cache@v3 - uses: actions/cache@v3
with: with:
path: | path: |

View File

@ -1,8 +1,11 @@
import com.github.triplet.gradle.androidpublisher.ReleaseStatus
import ru.cian.huawei.publish.ReleaseNote
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization' apply plugin: 'kotlinx-serialization'
apply plugin: 'kotlin-parcelize' apply plugin: 'kotlin-parcelize'
apply plugin: 'kotlin-kapt' apply plugin: 'com.google.devtools.ksp'
apply plugin: 'dagger.hilt.android.plugin' apply plugin: 'dagger.hilt.android.plugin'
apply plugin: 'com.google.gms.google-services' apply plugin: 'com.google.gms.google-services'
apply plugin: 'com.google.firebase.crashlytics' apply plugin: 'com.google.firebase.crashlytics'
@ -10,37 +13,29 @@ apply plugin: 'com.github.triplet.play'
apply plugin: 'ru.cian.huawei-publish' apply plugin: 'ru.cian.huawei-publish'
apply plugin: 'com.mikepenz.aboutlibraries.plugin' apply plugin: 'com.mikepenz.aboutlibraries.plugin'
apply plugin: 'com.huawei.agconnect' apply plugin: 'com.huawei.agconnect'
apply plugin: 'kotlin-kapt'
apply from: 'jacoco.gradle' apply from: 'jacoco.gradle'
apply from: 'sonarqube.gradle' apply from: 'sonarqube.gradle'
apply from: 'hooks.gradle' apply from: 'hooks.gradle'
android { android {
namespace 'io.github.wulkanowy' namespace 'io.github.wulkanowy'
compileSdkVersion 33 compileSdk 33
defaultConfig { defaultConfig {
applicationId "io.github.wulkanowy" applicationId "io.github.wulkanowy"
testApplicationId "io.github.tests.wulkanowy" testApplicationId "io.github.tests.wulkanowy"
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 33 targetSdkVersion 33
versionCode 123 versionCode 132
versionName "2.0.1" versionName "2.2.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resValue "string", "app_name", "Wulkanowy" resValue "string", "app_name", "Wulkanowy"
manifestPlaceholders = [ manifestPlaceholders = [
firebase_enabled: project.hasProperty("enableFirebase"), firebase_enabled: project.hasProperty("enableFirebase"),
admob_project_id: "" admob_project_id: ""
] ]
javaCompileOptions {
annotationProcessorOptions {
arguments += [
"room.schemaLocation": "$projectDir/schemas".toString(),
"room.incremental" : "true"
]
}
}
buildConfigField "String", "SINGLE_SUPPORT_AD_ID", "null" buildConfigField "String", "SINGLE_SUPPORT_AD_ID", "null"
buildConfigField "String", "DASHBOARD_TILE_AD_ID", "null" buildConfigField "String", "DASHBOARD_TILE_AD_ID", "null"
@ -73,6 +68,7 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release signingConfig signingConfigs.release
buildConfigField "String", "MESSAGES_BASE_URL", "\"https://messages.wulkanowy.net.pl\"" buildConfigField "String", "MESSAGES_BASE_URL", "\"https://messages.wulkanowy.net.pl\""
buildConfigField "String", "SCHOOLS_BASE_URL", '"https://schools.wulkanowy.net.pl"'
} }
debug { debug {
minifyEnabled false minifyEnabled false
@ -82,10 +78,11 @@ android {
versionNameSuffix "-dev" versionNameSuffix "-dev"
ext.enableCrashlytics = project.hasProperty("enableFirebase") ext.enableCrashlytics = project.hasProperty("enableFirebase")
buildConfigField "String", "MESSAGES_BASE_URL", "\"https://messages.wulkanowy.net.pl\"" buildConfigField "String", "MESSAGES_BASE_URL", "\"https://messages.wulkanowy.net.pl\""
buildConfigField "String", "SCHOOLS_BASE_URL", '"https://schools.wulkanowy.net.pl"'
} }
} }
flavorDimensions "platform" flavorDimensions += "platform"
productFlavors { productFlavors {
hms { hms {
@ -124,20 +121,20 @@ android {
} }
} }
testOptions.unitTests { testOptions {
includeAndroidResources = true unitTests.includeAndroidResources = true
// workaround HMS test errors https://github.com/robolectric/robolectric/issues/2750 // workaround HMS test errors https://github.com/robolectric/robolectric/issues/2750
all { jvmArgs '-noverify' } unitTests.all { jvmArgs '-noverify' }
} }
compileOptions { compileOptions {
coreLibraryDesugaringEnabled true coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_11 sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_11 targetCompatibility JavaVersion.VERSION_17
} }
kotlinOptions { kotlinOptions {
jvmTarget = "11" jvmTarget = "17"
freeCompilerArgs += ["-opt-in=kotlin.RequiresOptIn", "-Xjvm-default=all"] freeCompilerArgs += ["-opt-in=kotlin.RequiresOptIn", "-Xjvm-default=all"]
} }
@ -156,13 +153,16 @@ android {
kapt { kapt {
correctErrorTypes true correctErrorTypes true
} }
ksp {
arg("room.schemaLocation", "$projectDir/schemas".toString())
}
play { play {
defaultToAppBundles = false defaultToAppBundles = false
track = 'production' track = 'production'
releaseStatus = com.github.triplet.gradle.androidpublisher.ReleaseStatus.IN_PROGRESS releaseStatus = ReleaseStatus.IN_PROGRESS
userFraction = 0.10d userFraction = 0.01d
updatePriority = 2 updatePriority = 1
enabled.set(false) enabled.set(false)
} }
@ -171,7 +171,13 @@ huaweiPublish {
hmsRelease { hmsRelease {
credentialsPath = "$rootDir/app/src/release/agconnect-credentials.json" credentialsPath = "$rootDir/app/src/release/agconnect-credentials.json"
buildFormat = "aab" buildFormat = "aab"
deployType = "draft" deployType = "publish"
releaseNotes = [
new ReleaseNote(
"pl-PL",
"$projectDir/src/main/play/release-notes/pl-PL/default.txt"
)
]
} }
} }
} }
@ -179,34 +185,34 @@ huaweiPublish {
ext { ext {
work_manager = "2.8.1" work_manager = "2.8.1"
android_hilt = "1.0.0" android_hilt = "1.0.0"
room = "2.5.1" room = "2.5.2"
chucker = "3.5.2" chucker = "3.5.2"
mockk = "1.13.5" mockk = "1.13.8"
coroutines = "1.6.4" coroutines = "1.7.3"
} }
dependencies { dependencies {
implementation 'io.github.wulkanowy:sdk:2.0.1' implementation 'io.github.wulkanowy:sdk:2.2.0'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3'
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0" implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines"
implementation "androidx.core:core-ktx:1.10.0" implementation "androidx.core:core-ktx:1.10.1"
implementation 'androidx.core:core-splashscreen:1.0.1' implementation 'androidx.core:core-splashscreen:1.0.1'
implementation "androidx.activity:activity-ktx:1.7.1" implementation "androidx.activity:activity-ktx:1.7.2"
implementation "androidx.appcompat:appcompat:1.6.1" implementation "androidx.appcompat:appcompat:1.6.1"
implementation "androidx.fragment:fragment-ktx:1.5.7" implementation "androidx.fragment:fragment-ktx:1.6.1"
implementation "androidx.annotation:annotation:1.6.0" implementation "androidx.annotation:annotation:1.7.0"
implementation "androidx.preference:preference-ktx:1.2.0" implementation "androidx.preference:preference-ktx:1.2.1"
implementation "androidx.recyclerview:recyclerview:1.3.0" implementation "androidx.recyclerview:recyclerview:1.3.1"
implementation "androidx.viewpager2:viewpager2:1.1.0-beta01" implementation "androidx.viewpager2:viewpager2:1.1.0-beta02"
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
implementation "androidx.constraintlayout:constraintlayout:2.1.4" implementation "androidx.constraintlayout:constraintlayout:2.1.4"
implementation "androidx.coordinatorlayout:coordinatorlayout:1.2.0" implementation "androidx.coordinatorlayout:coordinatorlayout:1.2.0"
implementation "com.google.android.material:material:1.8.0" implementation "com.google.android.material:material:1.9.0"
implementation "com.github.wulkanowy:material-chips-input:2.3.1" implementation "com.github.wulkanowy:material-chips-input:2.3.1"
implementation "com.github.PhilJay:MPAndroidChart:v3.1.0" implementation "com.github.PhilJay:MPAndroidChart:v3.1.0"
implementation 'com.github.lopspower:CircularImageView:4.3.0' implementation 'com.github.lopspower:CircularImageView:4.3.0'
@ -214,11 +220,11 @@ dependencies {
implementation "androidx.work:work-runtime-ktx:$work_manager" implementation "androidx.work:work-runtime-ktx:$work_manager"
playImplementation "androidx.work:work-gcm:$work_manager" playImplementation "androidx.work:work-gcm:$work_manager"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.6.1" implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.6.2"
implementation "androidx.room:room-runtime:$room" implementation "androidx.room:room-runtime:$room"
implementation "androidx.room:room-ktx:$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" implementation "com.google.dagger:hilt-android:$hilt_version"
kapt "com.google.dagger:hilt-android-compiler:$hilt_version" kapt "com.google.dagger:hilt-android-compiler:$hilt_version"
@ -234,25 +240,26 @@ dependencies {
implementation "com.jakewharton.timber:timber:5.0.1" implementation "com.jakewharton.timber:timber:5.0.1"
implementation "at.favre.lib:slf4j-timber:1.0.1" implementation "at.favre.lib:slf4j-timber:1.0.1"
implementation 'com.github.bastienpaulfr:Treessence:1.0.5' implementation 'com.github.bastienpaulfr:Treessence:1.1.2'
implementation "com.mikepenz:aboutlibraries-core:$about_libraries" implementation "com.mikepenz:aboutlibraries-core:$about_libraries"
implementation "io.coil-kt:coil:2.3.0" implementation "io.coil-kt:coil:2.4.0"
implementation "io.github.wulkanowy:AppKillerManager:3.0.1" implementation "io.github.wulkanowy:AppKillerManager:3.0.1"
implementation 'me.xdrop:fuzzywuzzy:1.4.0' implementation 'me.xdrop:fuzzywuzzy:1.4.0'
implementation 'com.fredporciuncula:flow-preferences:1.9.1' implementation 'com.fredporciuncula:flow-preferences:1.9.1'
implementation 'org.apache.commons:commons-text:1.10.0' implementation 'org.apache.commons:commons-text:1.10.0'
playImplementation platform('com.google.firebase:firebase-bom:31.5.0') playImplementation platform('com.google.firebase:firebase-bom:32.3.1')
playImplementation 'com.google.firebase:firebase-analytics-ktx' playImplementation 'com.google.firebase:firebase-analytics-ktx'
playImplementation 'com.google.firebase:firebase-messaging:' playImplementation 'com.google.firebase:firebase-messaging:'
playImplementation 'com.google.firebase:firebase-crashlytics:' playImplementation 'com.google.firebase:firebase-crashlytics:'
playImplementation 'com.google.firebase:firebase-config-ktx' playImplementation 'com.google.firebase:firebase-config-ktx'
playImplementation 'com.google.android.play:core:1.10.3' playImplementation 'com.google.android.play:core:1.10.3'
playImplementation 'com.google.android.play:core-ktx:1.8.1' playImplementation 'com.google.android.play:core-ktx:1.8.1'
playImplementation 'com.google.android.gms:play-services-ads:22.0.0' playImplementation 'com.google.android.gms:play-services-ads:22.4.0'
playImplementation "com.google.android.play:integrity:1.2.0"
hmsImplementation 'com.huawei.hms:hianalytics:6.9.1.200' hmsImplementation 'com.huawei.hms:hianalytics:6.12.0.300'
hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.9.0.300' hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.9.1.301'
releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker" releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker"
@ -265,7 +272,7 @@ dependencies {
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines" testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines"
testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version" testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
testImplementation 'org.robolectric:robolectric:4.10' testImplementation 'org.robolectric:robolectric:4.10.3'
testImplementation "androidx.test:runner:1.5.2" testImplementation "androidx.test:runner:1.5.2"
testImplementation "androidx.test.ext:junit:1.1.5" testImplementation "androidx.test.ext:junit:1.1.5"
testImplementation "androidx.test:core:1.5.0" testImplementation "androidx.test:core:1.5.0"

View File

@ -1,5 +1,6 @@
# General # General
-dontobfuscate -dontobfuscate
-ignorewarnings
#Config for wulkanowy #Config for wulkanowy
@ -24,3 +25,18 @@
#Config for Material Components #Config for Material Components
-keep class com.google.android.material.tabs.** { *; } -keep class com.google.android.material.tabs.** { *; }
#Config for HMS SDK
-keepattributes *Annotation*
-keepattributes Exceptions
-keepattributes InnerClasses
-keepattributes Signature
-keep class com.huawei.agconnect.**{*;}
-keep class com.huawei.hianalytics.**{*;}
-keep class com.huawei.updatesdk.**{*;}
-keep class com.huawei.hms.**{*;}
#Config for Wulkanowy SDK
-keep,allowobfuscation,allowshrinking class retrofit2.Response

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,11 @@
package io.github.wulkanowy.utils
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class IntegrityHelper @Inject constructor() {
@Suppress("UNUSED_PARAMETER")
fun getIntegrityToken(requestId: String): String? = null
}

View File

@ -2,8 +2,8 @@ package io.github.wulkanowy.utils
import android.util.Log import android.util.Log
import com.huawei.agconnect.crash.AGConnectCrash import com.huawei.agconnect.crash.AGConnectCrash
import fr.bipi.tressence.base.FormatterPriorityTree import fr.bipi.treessence.base.FormatterPriorityTree
import fr.bipi.tressence.common.StackTraceRecorder import fr.bipi.treessence.common.StackTraceRecorder
class CrashLogTree : FormatterPriorityTree(Log.VERBOSE) { class CrashLogTree : FormatterPriorityTree(Log.VERBOSE) {

View File

@ -0,0 +1,11 @@
package io.github.wulkanowy.utils
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class IntegrityHelper @Inject constructor() {
@Suppress("UNUSED_PARAMETER")
fun getIntegrityToken(requestId: String): String? = null
}

View File

@ -50,5 +50,9 @@
{ {
"displayName": "Tomasz F.", "displayName": "Tomasz F.",
"githubUsername": "Pengwius" "githubUsername": "Pengwius"
},
{
"displayName": "Antoni Paduch",
"githubUsername": "janAte1"
} }
] ]

View File

@ -6,7 +6,7 @@ import androidx.hilt.work.HiltWorkerFactory
import androidx.work.Configuration import androidx.work.Configuration
import com.yariksoffice.lingver.Lingver import com.yariksoffice.lingver.Lingver
import dagger.hilt.android.HiltAndroidApp import dagger.hilt.android.HiltAndroidApp
import fr.bipi.tressence.file.FileLoggerTree import fr.bipi.treessence.file.FileLoggerTree
import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.ui.base.ThemeManager import io.github.wulkanowy.ui.base.ThemeManager
import io.github.wulkanowy.utils.* import io.github.wulkanowy.utils.*

View File

@ -14,6 +14,7 @@ import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import io.github.wulkanowy.data.api.AdminMessageService import io.github.wulkanowy.data.api.AdminMessageService
import io.github.wulkanowy.data.api.SchoolsService
import io.github.wulkanowy.data.db.AppDatabase import io.github.wulkanowy.data.db.AppDatabase
import io.github.wulkanowy.data.db.SharedPrefProvider import io.github.wulkanowy.data.db.SharedPrefProvider
import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.PreferencesRepository
@ -82,19 +83,29 @@ internal class DataModule {
@Singleton @Singleton
@Provides @Provides
fun provideRetrofit( fun provideAdminMessageService(
okHttpClient: OkHttpClient, okHttpClient: OkHttpClient,
json: Json, json: Json,
appInfo: AppInfo appInfo: AppInfo
): Retrofit = Retrofit.Builder() ): AdminMessageService = Retrofit.Builder()
.baseUrl(appInfo.messagesBaseUrl) .baseUrl(appInfo.messagesBaseUrl)
.client(okHttpClient) .client(okHttpClient)
.addConverterFactory(json.asConverterFactory("application/json".toMediaType())) .addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
.build() .build()
.create()
@Singleton @Singleton
@Provides @Provides
fun provideAdminMessageService(retrofit: Retrofit): AdminMessageService = retrofit.create() fun provideSchoolsService(
okHttpClient: OkHttpClient,
json: Json,
appInfo: AppInfo,
): SchoolsService = Retrofit.Builder()
.baseUrl(appInfo.schoolsBaseUrl)
.client(okHttpClient)
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
.build()
.create()
@Singleton @Singleton
@Provides @Provides

View File

@ -148,7 +148,7 @@ inline fun <ResultType, RequestType, T> networkBoundResource(
crossinline saveFetchResult: suspend (old: ResultType, new: RequestType) -> Unit, crossinline saveFetchResult: suspend (old: ResultType, new: RequestType) -> Unit,
crossinline onFetchFailed: (Throwable) -> Unit = { }, crossinline onFetchFailed: (Throwable) -> Unit = { },
crossinline shouldFetch: (ResultType) -> Boolean = { true }, crossinline shouldFetch: (ResultType) -> Boolean = { true },
crossinline mapResult: (ResultType) -> T crossinline mapResult: (ResultType) -> T,
) = flow { ) = flow {
emit(Resource.Loading()) emit(Resource.Loading())

View File

@ -0,0 +1,14 @@
package io.github.wulkanowy.data.api
import io.github.wulkanowy.data.pojos.IntegrityRequest
import io.github.wulkanowy.data.pojos.LoginEvent
import retrofit2.http.Body
import retrofit2.http.POST
import javax.inject.Singleton
@Singleton
interface SchoolsService {
@POST("/log/loginEvent")
suspend fun logLoginEvent(@Body request: IntegrityRequest<LoginEvent>)
}

View File

@ -49,6 +49,8 @@ import javax.inject.Singleton
AutoMigration(from = 47, to = 48), AutoMigration(from = 47, to = 48),
AutoMigration(from = 51, to = 52), AutoMigration(from = 51, to = 52),
AutoMigration(from = 54, to = 55, spec = Migration55::class), 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, version = AppDatabase.VERSION_SCHEMA,
exportSchema = true exportSchema = true
@ -57,7 +59,7 @@ import javax.inject.Singleton
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
companion object { companion object {
const val VERSION_SCHEMA = 55 const val VERSION_SCHEMA = 57
fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf( fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf(
Migration2(), Migration2(),

View File

@ -1,6 +1,7 @@
package io.github.wulkanowy.data.db package io.github.wulkanowy.data.db
import androidx.room.TypeConverter import androidx.room.TypeConverter
import io.github.wulkanowy.data.enums.MessageType
import io.github.wulkanowy.ui.modules.Destination import io.github.wulkanowy.ui.modules.Destination
import io.github.wulkanowy.utils.toTimestamp import io.github.wulkanowy.utils.toTimestamp
import kotlinx.serialization.SerializationException import kotlinx.serialization.SerializationException
@ -68,4 +69,9 @@ class Converters {
@TypeConverter @TypeConverter
fun stringToDestination(destination: String): Destination = json.decodeFromString(destination) fun stringToDestination(destination: String): Destination = json.decodeFromString(destination)
@TypeConverter
fun messageTypesToString(types: List<MessageType>): String = json.encodeToString(types)
@TypeConverter
fun stringToMessageTypes(text: String): List<MessageType> = json.decodeFromString(text)
} }

View File

@ -22,4 +22,4 @@ abstract class AdminMessageDao : BaseDao<AdminMessage> {
deleteAll(oldMessages) deleteAll(oldMessages)
insertAll(newMessages) insertAll(newMessages)
} }
} }

View File

@ -3,6 +3,7 @@ package io.github.wulkanowy.data.db.entities
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import io.github.wulkanowy.data.enums.MessageType
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
@ -33,7 +34,8 @@ data class AdminMessage(
val priority: String, val priority: String,
val type: String, @ColumnInfo(name = "types", defaultValue = "[]")
val types: List<MessageType> = emptyList(),
@ColumnInfo(name = "is_dismissible") @ColumnInfo(name = "is_dismissible")
val isDismissible: Boolean = false val isDismissible: Boolean = false

View File

@ -19,6 +19,9 @@ data class Student(
@ColumnInfo(name = "scrapper_base_url") @ColumnInfo(name = "scrapper_base_url")
val scrapperBaseUrl: String, val scrapperBaseUrl: String,
@ColumnInfo(name = "scrapper_domain_suffix", defaultValue = "")
val scrapperDomainSuffix: String,
@ColumnInfo(name = "mobile_base_url") @ColumnInfo(name = "mobile_base_url")
val mobileBaseUrl: String, val mobileBaseUrl: String,

View File

@ -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

View File

@ -0,0 +1,9 @@
package io.github.wulkanowy.data.enums
enum class MessageType {
GENERAL_MESSAGE,
DASHBOARD_MESSAGE,
LOGIN_MESSAGE,
PASS_RESET_MESSAGE,
ERROR_OVERRIDE,
}

View File

@ -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
}
}

View File

@ -55,6 +55,7 @@ fun SdkRegisterUser.mapToPojo(password: String?) = RegisterUser(
fun RegisterStudent.mapToStudentWithSemesters( fun RegisterStudent.mapToStudentWithSemesters(
user: RegisterUser, user: RegisterUser,
scrapperDomainSuffix: String,
symbol: RegisterSymbol, symbol: RegisterSymbol,
unit: RegisterUnit, unit: RegisterUnit,
colors: List<Long>, colors: List<Long>,
@ -76,6 +77,7 @@ fun RegisterStudent.mapToStudentWithSemesters(
studentName = "$studentName $studentSurname", studentName = "$studentName $studentSurname",
loginMode = user.loginMode.name, loginMode = user.loginMode.name,
scrapperBaseUrl = user.scrapperBaseUrl.orEmpty(), scrapperBaseUrl = user.scrapperBaseUrl.orEmpty(),
scrapperDomainSuffix = scrapperDomainSuffix,
mobileBaseUrl = symbol.hebeBaseUrl.orEmpty(), mobileBaseUrl = symbol.hebeBaseUrl.orEmpty(),
certificateKey = symbol.keyId.orEmpty(), certificateKey = symbol.keyId.orEmpty(),
privateKey = symbol.privatePem.orEmpty(), privateKey = symbol.privatePem.orEmpty(),

View File

@ -0,0 +1,21 @@
package io.github.wulkanowy.data.pojos
import kotlinx.serialization.Serializable
@Serializable
data class LoginEvent(
val uuid: String,
val schoolName: String,
val schoolShort: String,
val schoolAddress: String,
val scraperBaseUrl: String,
val symbol: String,
val schoolId: String,
val loginType: String,
)
@Serializable
data class IntegrityRequest<T>(
val tokenString: String,
val data: T,
)

View File

@ -1,10 +1,11 @@
package io.github.wulkanowy.data.repositories package io.github.wulkanowy.data.repositories
import io.github.wulkanowy.data.Resource
import io.github.wulkanowy.data.api.AdminMessageService import io.github.wulkanowy.data.api.AdminMessageService
import io.github.wulkanowy.data.db.dao.AdminMessageDao 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.data.networkBoundResource
import io.github.wulkanowy.utils.AppInfo import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -13,34 +14,20 @@ import javax.inject.Singleton
class AdminMessageRepository @Inject constructor( class AdminMessageRepository @Inject constructor(
private val adminMessageService: AdminMessageService, private val adminMessageService: AdminMessageService,
private val adminMessageDao: AdminMessageDao, private val adminMessageDao: AdminMessageDao,
private val appInfo: AppInfo
) { ) {
private val saveFetchResultMutex = Mutex() private val saveFetchResultMutex = Mutex()
suspend fun getAdminMessages(student: Student) = networkBoundResource( fun getAdminMessages(): Flow<Resource<List<AdminMessage>>> =
mutex = saveFetchResultMutex, networkBoundResource(
isResultEmpty = { it == null }, mutex = saveFetchResultMutex,
query = { adminMessageDao.loadAll() }, isResultEmpty = { false },
fetch = { adminMessageService.getAdminMessages() }, query = { adminMessageDao.loadAll() },
shouldFetch = { true }, fetch = { adminMessageService.getAdminMessages() },
saveFetchResult = { oldItems, newItems -> shouldFetch = { true },
adminMessageDao.removeOldAndSaveNew(oldItems, newItems) saveFetchResult = { oldItems, newItems ->
}, adminMessageDao.removeOldAndSaveNew(oldItems, newItems)
showSavedOnLoading = false, },
mapResult = { adminMessages -> showSavedOnLoading = false,
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 }
}
)
} }

View File

@ -3,18 +3,26 @@ package io.github.wulkanowy.data.repositories
import android.content.Context import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R 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.SharedPrefProvider
import io.github.wulkanowy.data.db.dao.MailboxDao import io.github.wulkanowy.data.db.dao.MailboxDao
import io.github.wulkanowy.data.db.dao.MessageAttachmentDao import io.github.wulkanowy.data.db.dao.MessageAttachmentDao
import io.github.wulkanowy.data.db.dao.MessagesDao 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
import io.github.wulkanowy.data.enums.MessageFolder.RECEIVED import io.github.wulkanowy.data.enums.MessageFolder.RECEIVED
import io.github.wulkanowy.data.enums.MessageFolder.TRASHED import io.github.wulkanowy.data.enums.MessageFolder.TRASHED
import io.github.wulkanowy.data.mappers.mapFromEntities import io.github.wulkanowy.data.mappers.mapFromEntities
import io.github.wulkanowy.data.mappers.mapToEntities 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.pojos.MessageDraft
import io.github.wulkanowy.data.waitForResult
import io.github.wulkanowy.domain.messages.GetMailboxByStudentUseCase import io.github.wulkanowy.domain.messages.GetMailboxByStudentUseCase
import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.sdk.pojo.Folder 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.Flow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import timber.log.Timber import timber.log.Timber
@ -97,7 +104,7 @@ class MessageRepository @Inject constructor(
shouldFetch = { shouldFetch = {
checkNotNull(it) { "This message no longer exist!" } checkNotNull(it) { "This message no longer exist!" }
Timber.d("Message content in db empty: ${it.message.content.isBlank()}") 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 = { query = {
messagesDb.loadMessageWithAttachment(message.messageGlobalKey) messagesDb.loadMessageWithAttachment(message.messageGlobalKey)
@ -113,7 +120,10 @@ class MessageRepository @Inject constructor(
messagesDb.updateAll( messagesDb.updateAll(
listOf(old.message.apply { listOf(old.message.apply {
id = message.id id = message.id
unread = !markAsRead unread = when {
markAsRead -> false
else -> unread
}
sender = new.sender sender = new.sender
recipients = new.recipients.singleOrNull() ?: "Wielu adresatów" recipients = new.recipients.singleOrNull() ?: "Wielu adresatów"
content = content.ifBlank { new.content } content = content.ifBlank { new.content }
@ -123,7 +133,7 @@ class MessageRepository @Inject constructor(
items = new.attachments.mapToEntities(message.messageGlobalKey), 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")
} }
) )

View File

@ -15,7 +15,6 @@ import io.github.wulkanowy.ui.modules.grade.GradeAverageMode
import io.github.wulkanowy.ui.modules.settings.appearance.menuorder.AppMenuItem import io.github.wulkanowy.ui.modules.settings.appearance.menuorder.AppMenuItem
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import java.time.Instant import java.time.Instant
@ -201,6 +200,14 @@ class PreferencesRepository @Inject constructor(
R.bool.pref_default_timetable_show_timers R.bool.pref_default_timetable_show_timers
) )
val showTimetableGaps: TimetableGapsMode
get() = TimetableGapsMode.getByValue(
getString(
R.string.pref_key_timetable_show_gaps,
R.string.pref_default_timetable_show_gaps
)
)
val showSubjectsWithoutGrades: Boolean val showSubjectsWithoutGrades: Boolean
get() = getBoolean( get() = getBoolean(
R.string.pref_key_subjects_without_grades, R.string.pref_key_subjects_without_grades,
@ -343,6 +350,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 var installationId: String
get() = sharedPref.getString(PREF_KEY_INSTALLATION_ID, null).orEmpty() get() = sharedPref.getString(PREF_KEY_INSTALLATION_ID, null).orEmpty()
private set(value) = sharedPref.edit { putString(PREF_KEY_INSTALLATION_ID, value) } private set(value) = sharedPref.edit { putString(PREF_KEY_INSTALLATION_ID, value) }

View File

@ -0,0 +1,68 @@
package io.github.wulkanowy.data.repositories
import io.github.wulkanowy.data.api.SchoolsService
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.data.pojos.IntegrityRequest
import io.github.wulkanowy.data.pojos.LoginEvent
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.ui.modules.login.LoginData
import io.github.wulkanowy.utils.IntegrityHelper
import io.github.wulkanowy.utils.getCurrentOrLast
import io.github.wulkanowy.utils.init
import kotlinx.coroutines.withTimeout
import timber.log.Timber
import java.util.UUID
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.time.Duration.Companion.seconds
@Singleton
class SchoolsRepository @Inject constructor(
private val integrityHelper: IntegrityHelper,
private val schoolsService: SchoolsService,
private val sdk: Sdk,
) {
suspend fun logSchoolLogin(loginData: LoginData, students: List<StudentWithSemesters>) {
students.forEach {
runCatching {
withTimeout(10.seconds) {
logLogin(loginData, it.student, it.semesters.getCurrentOrLast())
}
}
.onFailure { Timber.e(it) }
}
}
private suspend fun logLogin(loginData: LoginData, student: Student, semester: Semester) {
val requestId = UUID.randomUUID().toString()
val token = integrityHelper.getIntegrityToken(requestId) ?: return
val schoolInfo = sdk
.init(student.copy(password = loginData.password))
.switchDiary(
diaryId = semester.diaryId,
kindergartenDiaryId = semester.kindergartenDiaryId,
schoolYear = semester.schoolYear
)
.getSchool()
schoolsService.logLoginEvent(
IntegrityRequest(
tokenString = token,
data = LoginEvent(
uuid = requestId,
schoolAddress = schoolInfo.address,
schoolName = schoolInfo.name,
schoolShort = student.schoolShortName,
scraperBaseUrl = student.scrapperBaseUrl,
loginType = student.loginType,
symbol = student.symbol,
schoolId = student.schoolSymbol,
)
)
)
}
}

View File

@ -41,7 +41,7 @@ class SemesterRepository @Inject constructor(
val isRefreshOnModeChangeRequired = when { val isRefreshOnModeChangeRequired = when {
Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE -> { Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE -> {
semesters.firstOrNull { it.isCurrent }?.let { semesters.firstOrNull { it.isCurrent() }?.let {
0 == it.diaryId && 0 == it.kindergartenDiaryId 0 == it.diaryId && 0 == it.kindergartenDiaryId
} == true } == true
} }
@ -49,7 +49,7 @@ class SemesterRepository @Inject constructor(
} }
val isRefreshOnNoCurrentAppropriate = val isRefreshOnNoCurrentAppropriate =
refreshOnNoCurrent && !semesters.any { semester -> semester.isCurrent } refreshOnNoCurrent && !semesters.any { semester -> semester.isCurrent() }
return forceRefresh || isNoSemesters || isRefreshOnModeChangeRequired || isRefreshOnNoCurrentAppropriate return forceRefresh || isNoSemesters || isRefreshOnModeChangeRequired || isRefreshOnNoCurrentAppropriate
} }

View File

@ -43,22 +43,14 @@ class StudentRepository @Inject constructor(
.getStudentsFromHebe(token, pin, symbol, "") .getStudentsFromHebe(token, pin, symbol, "")
.mapToPojo(null) .mapToPojo(null)
suspend fun getStudentsScrapper(
email: String,
password: String,
scrapperBaseUrl: String,
symbol: String
): RegisterUser = sdk
.getUserSubjectsFromScrapper(email, password, scrapperBaseUrl, symbol)
.mapToPojo(password)
suspend fun getUserSubjectsFromScrapper( suspend fun getUserSubjectsFromScrapper(
email: String, email: String,
password: String, password: String,
scrapperBaseUrl: String, scrapperBaseUrl: String,
domainSuffix: String,
symbol: String symbol: String
): RegisterUser = sdk ): RegisterUser = sdk
.getUserSubjectsFromScrapper(email, password, scrapperBaseUrl, symbol) .getUserSubjectsFromScrapper(email, password, scrapperBaseUrl, domainSuffix, symbol)
.mapToPojo(password) .mapToPojo(password)
suspend fun getStudentsHybrid( suspend fun getStudentsHybrid(

View File

@ -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<Resource<AdminMessage?>> {
return invoke(student.scrapperBaseUrl, type)
}
operator fun invoke(scrapperBaseUrl: String, type: MessageType): Flow<Resource<AdminMessage?>> {
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
}
}

View File

@ -4,6 +4,7 @@ import android.content.Intent
import android.widget.RemoteViewsService import android.widget.RemoteViewsService
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.data.db.SharedPrefProvider 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.SemesterRepository
import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.repositories.TimetableRepository import io.github.wulkanowy.data.repositories.TimetableRepository
@ -26,10 +27,19 @@ class TimetableWidgetService : RemoteViewsService() {
@Inject @Inject
lateinit var sharedPref: SharedPrefProvider lateinit var sharedPref: SharedPrefProvider
@Inject
lateinit var prefRepository: PreferencesRepository
override fun onGetViewFactory(intent: Intent?): RemoteViewsFactory { override fun onGetViewFactory(intent: Intent?): RemoteViewsFactory {
Timber.d("TimetableWidgetFactory created") Timber.d("TimetableWidgetFactory created")
return TimetableWidgetFactory( return TimetableWidgetFactory(
timetableRepo, studentRepo, semesterRepo, sharedPref, applicationContext, intent timetableRepository = timetableRepo,
studentRepository = studentRepo,
semesterRepository = semesterRepo,
sharedPref = sharedPref,
prefRepository = prefRepository,
context = applicationContext,
intent = intent,
) )
} }
} }

View File

@ -3,6 +3,7 @@ package io.github.wulkanowy.ui.modules.dashboard
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import io.github.wulkanowy.ui.modules.dashboard.adapters.DashboardAdapter import io.github.wulkanowy.ui.modules.dashboard.adapters.DashboardAdapter
import io.github.wulkanowy.ui.modules.dashboard.viewholders.AdminMessageViewHolder
import java.util.* import java.util.*
class DashboardItemMoveCallback( class DashboardItemMoveCallback(
@ -55,5 +56,5 @@ class DashboardItemMoveCallback(
} }
private val RecyclerView.ViewHolder.isAdminMessageOrAccountItem: Boolean private val RecyclerView.ViewHolder.isAdminMessageOrAccountItem: Boolean
get() = this is DashboardAdapter.AdminMessageViewHolder || this is DashboardAdapter.AccountViewHolder get() = this is AdminMessageViewHolder || this is DashboardAdapter.AccountViewHolder
} }

View File

@ -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.LuckyNumber
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.enums.MessageFolder import io.github.wulkanowy.data.enums.MessageFolder
import io.github.wulkanowy.data.enums.MessageType
import io.github.wulkanowy.data.repositories.* 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.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.utils.AdsHelper import io.github.wulkanowy.utils.AdsHelper
@ -32,7 +34,7 @@ class DashboardPresenter @Inject constructor(
private val conferenceRepository: ConferenceRepository, private val conferenceRepository: ConferenceRepository,
private val preferencesRepository: PreferencesRepository, private val preferencesRepository: PreferencesRepository,
private val schoolAnnouncementRepository: SchoolAnnouncementRepository, private val schoolAnnouncementRepository: SchoolAnnouncementRepository,
private val adminMessageRepository: AdminMessageRepository, private val getAppropriateAdminMessageUseCase: GetAppropriateAdminMessageUseCase,
private val adsHelper: AdsHelper private val adsHelper: AdsHelper
) : BasePresenter<DashboardView>(errorHandler, studentRepository) { ) : BasePresenter<DashboardView>(errorHandler, studentRepository) {
@ -159,19 +161,23 @@ class DashboardPresenter @Inject constructor(
DashboardItem.Type.ACCOUNT -> { DashboardItem.Type.ACCOUNT -> {
updateData(DashboardItem.Account(student), forceRefresh) updateData(DashboardItem.Account(student), forceRefresh)
} }
DashboardItem.Type.HORIZONTAL_GROUP -> { DashboardItem.Type.HORIZONTAL_GROUP -> {
loadHorizontalGroup(student, forceRefresh) loadHorizontalGroup(student, forceRefresh)
} }
DashboardItem.Type.LESSONS -> loadLessons(student, forceRefresh) DashboardItem.Type.LESSONS -> loadLessons(student, forceRefresh)
DashboardItem.Type.GRADES -> loadGrades(student, forceRefresh) DashboardItem.Type.GRADES -> loadGrades(student, forceRefresh)
DashboardItem.Type.HOMEWORK -> loadHomework(student, forceRefresh) DashboardItem.Type.HOMEWORK -> loadHomework(student, forceRefresh)
DashboardItem.Type.ANNOUNCEMENTS -> { DashboardItem.Type.ANNOUNCEMENTS -> {
loadSchoolAnnouncements(student, forceRefresh) loadSchoolAnnouncements(student, forceRefresh)
} }
DashboardItem.Type.EXAMS -> loadExams(student, forceRefresh) DashboardItem.Type.EXAMS -> loadExams(student, forceRefresh)
DashboardItem.Type.CONFERENCES -> { DashboardItem.Type.CONFERENCES -> {
loadConferences(student, forceRefresh) loadConferences(student, forceRefresh)
} }
DashboardItem.Type.ADS -> loadAds(forceRefresh) DashboardItem.Type.ADS -> loadAds(forceRefresh)
DashboardItem.Type.ADMIN_MESSAGE -> loadAdminMessage(student, forceRefresh) DashboardItem.Type.ADMIN_MESSAGE -> loadAdminMessage(student, forceRefresh)
} }
@ -355,6 +361,7 @@ class DashboardPresenter @Inject constructor(
firstLoadedItemList += DashboardItem.Type.GRADES firstLoadedItemList += DashboardItem.Type.GRADES
} }
} }
is Resource.Success -> { is Resource.Success -> {
Timber.i("Loading dashboard grades result: Success") Timber.i("Loading dashboard grades result: Success")
updateData( updateData(
@ -365,6 +372,7 @@ class DashboardPresenter @Inject constructor(
forceRefresh forceRefresh
) )
} }
is Resource.Error -> { is Resource.Error -> {
Timber.i("Loading dashboard grades result: An exception occurred") Timber.i("Loading dashboard grades result: An exception occurred")
errorHandler.dispatch(it.error) errorHandler.dispatch(it.error)
@ -402,12 +410,14 @@ class DashboardPresenter @Inject constructor(
firstLoadedItemList += DashboardItem.Type.LESSONS firstLoadedItemList += DashboardItem.Type.LESSONS
} }
} }
is Resource.Success -> { is Resource.Success -> {
Timber.i("Loading dashboard lessons result: Success") Timber.i("Loading dashboard lessons result: Success")
updateData( updateData(
DashboardItem.Lessons(it.data), forceRefresh DashboardItem.Lessons(it.data), forceRefresh
) )
} }
is Resource.Error -> { is Resource.Error -> {
Timber.i("Loading dashboard lessons result: An exception occurred") Timber.i("Loading dashboard lessons result: An exception occurred")
errorHandler.dispatch(it.error) errorHandler.dispatch(it.error)
@ -457,10 +467,12 @@ class DashboardPresenter @Inject constructor(
firstLoadedItemList += DashboardItem.Type.HOMEWORK firstLoadedItemList += DashboardItem.Type.HOMEWORK
} }
} }
is Resource.Success -> { is Resource.Success -> {
Timber.i("Loading dashboard homework result: Success") Timber.i("Loading dashboard homework result: Success")
updateData(DashboardItem.Homework(it.data), forceRefresh) updateData(DashboardItem.Homework(it.data), forceRefresh)
} }
is Resource.Error -> { is Resource.Error -> {
Timber.i("Loading dashboard homework result: An exception occurred") Timber.i("Loading dashboard homework result: An exception occurred")
errorHandler.dispatch(it.error) errorHandler.dispatch(it.error)
@ -489,10 +501,12 @@ class DashboardPresenter @Inject constructor(
firstLoadedItemList += DashboardItem.Type.ANNOUNCEMENTS firstLoadedItemList += DashboardItem.Type.ANNOUNCEMENTS
} }
} }
is Resource.Success -> { is Resource.Success -> {
Timber.i("Loading dashboard announcements result: Success") Timber.i("Loading dashboard announcements result: Success")
updateData(DashboardItem.Announcements(it.data), forceRefresh) updateData(DashboardItem.Announcements(it.data), forceRefresh)
} }
is Resource.Error -> { is Resource.Error -> {
Timber.i("Loading dashboard announcements result: An exception occurred") Timber.i("Loading dashboard announcements result: An exception occurred")
errorHandler.dispatch(it.error) errorHandler.dispatch(it.error)
@ -530,10 +544,12 @@ class DashboardPresenter @Inject constructor(
firstLoadedItemList += DashboardItem.Type.EXAMS firstLoadedItemList += DashboardItem.Type.EXAMS
} }
} }
is Resource.Success -> { is Resource.Success -> {
Timber.i("Loading dashboard exams result: Success") Timber.i("Loading dashboard exams result: Success")
updateData(DashboardItem.Exams(it.data), forceRefresh) updateData(DashboardItem.Exams(it.data), forceRefresh)
} }
is Resource.Error -> { is Resource.Error -> {
Timber.i("Loading dashboard exams result: An exception occurred") Timber.i("Loading dashboard exams result: An exception occurred")
errorHandler.dispatch(it.error) errorHandler.dispatch(it.error)
@ -569,10 +585,12 @@ class DashboardPresenter @Inject constructor(
firstLoadedItemList += DashboardItem.Type.CONFERENCES firstLoadedItemList += DashboardItem.Type.CONFERENCES
} }
} }
is Resource.Success -> { is Resource.Success -> {
Timber.i("Loading dashboard conferences result: Success") Timber.i("Loading dashboard conferences result: Success")
updateData(DashboardItem.Conferences(it.data), forceRefresh) updateData(DashboardItem.Conferences(it.data), forceRefresh)
} }
is Resource.Error -> { is Resource.Error -> {
Timber.i("Loading dashboard conferences result: An exception occurred") Timber.i("Loading dashboard conferences result: An exception occurred")
errorHandler.dispatch(it.error) errorHandler.dispatch(it.error)
@ -584,12 +602,12 @@ class DashboardPresenter @Inject constructor(
} }
private fun loadAdminMessage(student: Student, forceRefresh: Boolean) { private fun loadAdminMessage(student: Student, forceRefresh: Boolean) {
flatResourceFlow { adminMessageRepository.getAdminMessages(student) } flatResourceFlow {
.filter { getAppropriateAdminMessageUseCase(
val data = it.dataOrNull ?: return@filter true student = student,
val isDismissed = data.id in preferencesRepository.dismissedAdminMessageIds type = MessageType.DASHBOARD_MESSAGE,
!isDismissed )
} }
.onEach { .onEach {
when (it) { when (it) {
is Resource.Loading -> { is Resource.Loading -> {
@ -597,6 +615,7 @@ class DashboardPresenter @Inject constructor(
if (forceRefresh) return@onEach if (forceRefresh) return@onEach
updateData(DashboardItem.AdminMessages(), forceRefresh) updateData(DashboardItem.AdminMessages(), forceRefresh)
} }
is Resource.Success -> { is Resource.Success -> {
Timber.i("Loading dashboard admin message result: Success") Timber.i("Loading dashboard admin message result: Success")
updateData( updateData(
@ -604,6 +623,7 @@ class DashboardPresenter @Inject constructor(
forceRefresh = forceRefresh forceRefresh = forceRefresh
) )
} }
is Resource.Error -> { is Resource.Error -> {
Timber.i("Loading dashboard admin message result: An exception occurred") Timber.i("Loading dashboard admin message result: An exception occurred")
Timber.e(it.error) Timber.e(it.error)

View File

@ -1,8 +1,6 @@
package io.github.wulkanowy.ui.modules.dashboard.adapters package io.github.wulkanowy.ui.modules.dashboard.adapters
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.res.ColorStateList
import android.graphics.Color
import android.graphics.Typeface import android.graphics.Typeface
import android.os.Handler import android.os.Handler
import android.os.Looper 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.data.enums.GradeColorTheme
import io.github.wulkanowy.databinding.* import io.github.wulkanowy.databinding.*
import io.github.wulkanowy.ui.modules.dashboard.DashboardItem import io.github.wulkanowy.ui.modules.dashboard.DashboardItem
import io.github.wulkanowy.ui.modules.dashboard.viewholders.AdminMessageViewHolder
import io.github.wulkanowy.utils.* import io.github.wulkanowy.utils.*
import timber.log.Timber import timber.log.Timber
import java.time.Duration import java.time.Duration
@ -109,7 +108,9 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
ItemDashboardConferencesBinding.inflate(inflater, parent, false) ItemDashboardConferencesBinding.inflate(inflater, parent, false)
) )
DashboardItem.Type.ADMIN_MESSAGE.ordinal -> AdminMessageViewHolder( DashboardItem.Type.ADMIN_MESSAGE.ordinal -> AdminMessageViewHolder(
ItemDashboardAdminMessageBinding.inflate(inflater, parent, false) ItemDashboardAdminMessageBinding.inflate(inflater, parent, false),
onAdminMessageDismissClickListener = onAdminMessageDismissClickListener,
onAdminMessageClickListener = onAdminMessageClickListener,
) )
DashboardItem.Type.ADS.ordinal -> AdsViewHolder( DashboardItem.Type.ADS.ordinal -> AdsViewHolder(
ItemDashboardAdsBinding.inflate(inflater, parent, false) ItemDashboardAdsBinding.inflate(inflater, parent, false)
@ -128,7 +129,7 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
is AnnouncementsViewHolder -> bindAnnouncementsViewHolder(holder, position) is AnnouncementsViewHolder -> bindAnnouncementsViewHolder(holder, position)
is ExamsViewHolder -> bindExamsViewHolder(holder, position) is ExamsViewHolder -> bindExamsViewHolder(holder, position)
is ConferencesViewHolder -> bindConferencesViewHolder(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) is AdsViewHolder -> bindAdsViewHolder(holder, position)
} }
} }
@ -733,38 +734,6 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
} }
} }
private fun bindAdminMessage(adminMessageViewHolder: AdminMessageViewHolder, position: Int) {
val item = (items[position] as DashboardItem.AdminMessages).adminMessage ?: return
val context = adminMessageViewHolder.binding.root.context
val (backgroundColor, textColor) = when (item.priority) {
"HIGH" -> {
context.getThemeAttrColor(R.attr.colorPrimary) to
context.getThemeAttrColor(R.attr.colorOnPrimary)
}
"MEDIUM" -> {
context.getThemeAttrColor(R.attr.colorMessageMedium) to Color.BLACK
}
else -> null to context.getThemeAttrColor(R.attr.colorOnSurface)
}
with(adminMessageViewHolder.binding) {
dashboardAdminMessageItemTitle.text = item.title
dashboardAdminMessageItemTitle.setTextColor(textColor)
dashboardAdminMessageItemDescription.text = item.content
dashboardAdminMessageItemDescription.setTextColor(textColor)
dashboardAdminMessageItemIcon.setColorFilter(textColor)
dashboardAdminMessageItemDismiss.isVisible = item.isDismissible
dashboardAdminMessageItemDismiss.setOnClickListener {
onAdminMessageDismissClickListener(item)
}
root.setCardBackgroundColor(backgroundColor?.let { ColorStateList.valueOf(it) })
item.destinationUrl?.let { url ->
root.setOnClickListener { onAdminMessageClickListener(url) }
}
}
}
private fun bindAdsViewHolder(adsViewHolder: AdsViewHolder, position: Int) { private fun bindAdsViewHolder(adsViewHolder: AdsViewHolder, position: Int) {
val item = (items[position] as DashboardItem.Ads).adBanner ?: return val item = (items[position] as DashboardItem.Ads).adBanner ?: return
val binding = adsViewHolder.binding val binding = adsViewHolder.binding
@ -818,9 +787,6 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
val adapter by lazy { DashboardConferencesAdapter() } val adapter by lazy { DashboardConferencesAdapter() }
} }
class AdminMessageViewHolder(val binding: ItemDashboardAdminMessageBinding) :
RecyclerView.ViewHolder(binding.root)
class AdsViewHolder(val binding: ItemDashboardAdsBinding) : class AdsViewHolder(val binding: ItemDashboardAdsBinding) :
RecyclerView.ViewHolder(binding.root) RecyclerView.ViewHolder(binding.root)

View File

@ -0,0 +1,52 @@
package io.github.wulkanowy.ui.modules.dashboard.viewholders
import android.content.res.ColorStateList
import android.graphics.Color
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.AdminMessage
import io.github.wulkanowy.databinding.ItemDashboardAdminMessageBinding
import io.github.wulkanowy.ui.modules.dashboard.DashboardItem
import io.github.wulkanowy.utils.getThemeAttrColor
class AdminMessageViewHolder(
private val binding: ItemDashboardAdminMessageBinding,
private val onAdminMessageDismissClickListener: (AdminMessage) -> 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) }
}
}
}
}

View File

@ -58,7 +58,7 @@ class GradeAverageProvider @Inject constructor(
when (params.gradeAverageMode) { when (params.gradeAverageMode) {
ONE_SEMESTER -> getGradeSubjects( ONE_SEMESTER -> getGradeSubjects(
student = student, student = student,
semester = semesters.single { it.semesterId == semesterId }, semester = semesters.first { it.semesterId == semesterId },
forceRefresh = forceRefresh, forceRefresh = forceRefresh,
params = params, params = params,
) )

View File

@ -6,5 +6,6 @@ data class LoginData(
val login: String, val login: String,
val password: String, val password: String,
val baseUrl: String, val baseUrl: String,
val domainSuffix: String,
val symbol: String?, val symbol: String?,
) : Serializable ) : Serializable

View File

@ -5,6 +5,7 @@ import android.view.View
import android.view.View.GONE import android.view.View.GONE
import android.view.View.VISIBLE import android.view.View.VISIBLE
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import androidx.core.view.isVisible
import androidx.core.widget.doOnTextChanged import androidx.core.widget.doOnTextChanged
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R import io.github.wulkanowy.R
@ -55,6 +56,9 @@ class LoginAdvancedFragment :
get() = hostValues.getOrNull(hostKeys.indexOf(binding.loginFormHost.text.toString())) get() = hostValues.getOrNull(hostKeys.indexOf(binding.loginFormHost.text.toString()))
.orEmpty() .orEmpty()
override val formDomainSuffix: String
get() = binding.loginFormDomainSuffix.text.toString().trim()
override val formHostSymbol: String override val formHostSymbol: String
get() = hostSymbols.getOrNull(hostKeys.indexOf(binding.loginFormHost.text.toString())) get() = hostSymbols.getOrNull(hostKeys.indexOf(binding.loginFormHost.text.toString()))
.orEmpty() .orEmpty()
@ -279,6 +283,7 @@ class LoginAdvancedFragment :
loginFormUsernameLayout.visibility = VISIBLE loginFormUsernameLayout.visibility = VISIBLE
loginFormPassLayout.visibility = VISIBLE loginFormPassLayout.visibility = VISIBLE
loginFormHostLayout.visibility = VISIBLE loginFormHostLayout.visibility = VISIBLE
loginFormDomainSuffixLayout.isVisible = true
loginFormPinLayout.visibility = GONE loginFormPinLayout.visibility = GONE
loginFormSymbolLayout.visibility = VISIBLE loginFormSymbolLayout.visibility = VISIBLE
loginFormTokenLayout.visibility = GONE loginFormTokenLayout.visibility = GONE
@ -290,6 +295,7 @@ class LoginAdvancedFragment :
loginFormUsernameLayout.visibility = VISIBLE loginFormUsernameLayout.visibility = VISIBLE
loginFormPassLayout.visibility = VISIBLE loginFormPassLayout.visibility = VISIBLE
loginFormHostLayout.visibility = VISIBLE loginFormHostLayout.visibility = VISIBLE
loginFormDomainSuffixLayout.isVisible = true
loginFormPinLayout.visibility = GONE loginFormPinLayout.visibility = GONE
loginFormSymbolLayout.visibility = VISIBLE loginFormSymbolLayout.visibility = VISIBLE
loginFormTokenLayout.visibility = GONE loginFormTokenLayout.visibility = GONE
@ -301,6 +307,7 @@ class LoginAdvancedFragment :
loginFormUsernameLayout.visibility = GONE loginFormUsernameLayout.visibility = GONE
loginFormPassLayout.visibility = GONE loginFormPassLayout.visibility = GONE
loginFormHostLayout.visibility = GONE loginFormHostLayout.visibility = GONE
loginFormDomainSuffixLayout.isVisible = false
loginFormPinLayout.visibility = VISIBLE loginFormPinLayout.visibility = VISIBLE
loginFormSymbolLayout.visibility = VISIBLE loginFormSymbolLayout.visibility = VISIBLE
loginFormTokenLayout.visibility = VISIBLE loginFormTokenLayout.visibility = VISIBLE

View File

@ -154,6 +154,7 @@ class LoginAdvancedPresenter @Inject constructor(
login = view?.formUsernameValue.orEmpty().trim(), login = view?.formUsernameValue.orEmpty().trim(),
password = view?.formPassValue.orEmpty().trim(), password = view?.formPassValue.orEmpty().trim(),
baseUrl = view?.formHostValue.orEmpty().trim(), baseUrl = view?.formHostValue.orEmpty().trim(),
domainSuffix = view?.formDomainSuffix.orEmpty().trim(),
symbol = view?.formSymbolValue.orEmpty().trim().getNormalizedSymbol(), symbol = view?.formSymbolValue.orEmpty().trim().getNormalizedSymbol(),
) )
when (it.data.symbols.size) { when (it.data.symbols.size) {
@ -186,6 +187,7 @@ class LoginAdvancedPresenter @Inject constructor(
val email = view?.formUsernameValue.orEmpty() val email = view?.formUsernameValue.orEmpty()
val password = view?.formPassValue.orEmpty() val password = view?.formPassValue.orEmpty()
val endpoint = view?.formHostValue.orEmpty() val endpoint = view?.formHostValue.orEmpty()
val domainSuffix = view?.formDomainSuffix.orEmpty()
val pin = view?.formPinValue.orEmpty() val pin = view?.formPinValue.orEmpty()
val symbol = view?.formSymbolValue.orEmpty() val symbol = view?.formSymbolValue.orEmpty()
@ -193,8 +195,8 @@ class LoginAdvancedPresenter @Inject constructor(
return when (Sdk.Mode.valueOf(view?.formLoginType.orEmpty())) { return when (Sdk.Mode.valueOf(view?.formLoginType.orEmpty())) {
Sdk.Mode.HEBE -> studentRepository.getStudentsApi(pin, symbol, token) Sdk.Mode.HEBE -> studentRepository.getStudentsApi(pin, symbol, token)
Sdk.Mode.SCRAPPER -> studentRepository.getStudentsScrapper( Sdk.Mode.SCRAPPER -> studentRepository.getUserSubjectsFromScrapper(
email, password, endpoint, symbol email, password, endpoint, domainSuffix, symbol
) )
Sdk.Mode.HYBRID -> studentRepository.getStudentsHybrid( Sdk.Mode.HYBRID -> studentRepository.getStudentsHybrid(

View File

@ -12,6 +12,8 @@ interface LoginAdvancedView : BaseView {
val formHostValue: String val formHostValue: String
val formDomainSuffix: String
val formHostSymbol: String val formHostSymbol: String
val formLoginType: String val formLoginType: String

View File

@ -9,12 +9,16 @@ import androidx.core.view.isVisible
import androidx.core.widget.doOnTextChanged import androidx.core.widget.doOnTextChanged
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R 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.pojos.RegisterUser
import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.databinding.FragmentLoginFormBinding import io.github.wulkanowy.databinding.FragmentLoginFormBinding
import io.github.wulkanowy.ui.base.BaseFragment 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.LoginActivity
import io.github.wulkanowy.ui.modules.login.LoginData import io.github.wulkanowy.ui.modules.login.LoginData
import io.github.wulkanowy.ui.modules.login.support.LoginSupportDialog
import io.github.wulkanowy.ui.modules.login.support.LoginSupportInfo
import io.github.wulkanowy.utils.* import io.github.wulkanowy.utils.*
import javax.inject.Inject import javax.inject.Inject
@ -45,6 +49,9 @@ class LoginFormFragment : BaseFragment<FragmentLoginFormBinding>(R.layout.fragme
get() = hostValues.getOrNull(hostKeys.indexOf(binding.loginFormHost.text.toString())) get() = hostValues.getOrNull(hostKeys.indexOf(binding.loginFormHost.text.toString()))
.orEmpty() .orEmpty()
override val formDomainSuffix: String
get() = binding.loginFormDomainSuffix.text.toString()
override val formHostSymbol: String override val formHostSymbol: String
get() = hostSymbols.getOrNull(hostKeys.indexOf(binding.loginFormHost.text.toString())) get() = hostSymbols.getOrNull(hostKeys.indexOf(binding.loginFormHost.text.toString()))
.orEmpty() .orEmpty()
@ -179,7 +186,9 @@ class LoginFormFragment : BaseFragment<FragmentLoginFormBinding>(R.layout.fragme
override fun clearPassError() { override fun clearPassError() {
binding.loginFormPassLayout.error = null binding.loginFormPassLayout.error = null
binding.loginFormPassLayout.setEndIconTintList(null) binding.loginFormPassLayout.setEndIconTintList(
requireContext().getAttrColorStateList(R.attr.colorOnSurface)
)
binding.loginFormErrorBox.isVisible = false binding.loginFormErrorBox.isVisible = false
} }
@ -204,6 +213,23 @@ class LoginFormFragment : BaseFragment<FragmentLoginFormBinding>(R.layout.fragme
binding.loginFormContainer.visibility = if (show) VISIBLE else GONE binding.loginFormContainer.visibility = if (show) VISIBLE else GONE
} }
override fun showAdminMessage(message: AdminMessage?) {
AdminMessageViewHolder(
binding = binding.loginFormMessage,
onAdminMessageDismissClickListener = presenter::onAdminMessageDismissed,
onAdminMessageClickListener = presenter::onAdminMessageSelected,
).bind(message)
binding.loginFormMessage.root.isVisible = message != null
}
override fun openInternetBrowser(url: String) {
requireContext().openInternetBrowser(url)
}
override fun showDomainSuffixInput(show: Boolean) {
binding.loginFormDomainSuffixLayout.isVisible = show
}
override fun showOtherOptionsButton(show: Boolean) { override fun showOtherOptionsButton(show: Boolean) {
binding.loginFormAdvancedButton.isVisible = show binding.loginFormAdvancedButton.isVisible = show
} }
@ -214,8 +240,7 @@ class LoginFormFragment : BaseFragment<FragmentLoginFormBinding>(R.layout.fragme
} }
override fun showContact(show: Boolean) { override fun showContact(show: Boolean) {
binding.loginFormContact.visibility = if (show) VISIBLE else GONE binding.loginFormContact.isVisible = show
binding.loginFormRecoverLink.visibility = if (show) GONE else VISIBLE
} }
override fun openPrivacyPolicyPage() { override fun openPrivacyPolicyPage() {
@ -256,22 +281,10 @@ class LoginFormFragment : BaseFragment<FragmentLoginFormBinding>(R.layout.fragme
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
presenter.updateUsernameLabel() presenter.updateUsernameLabel()
presenter.updateCustomDomainSuffixVisibility()
} }
override fun openEmail(lastError: String) { override fun openEmail(supportInfo: LoginSupportInfo) {
context?.openEmailClient( LoginSupportDialog.newInstance(supportInfo).show(childFragmentManager, "support_dialog")
chooserTitle = requireContext().getString(R.string.login_email_intent_title),
email = "wulkanowyinc@gmail.com",
subject = requireContext().getString(R.string.login_email_subject),
body = requireContext().getString(
R.string.login_email_text,
"${appInfo.systemManufacturer} ${appInfo.systemModel}",
appInfo.systemVersion.toString(),
"${appInfo.versionName}-${appInfo.buildFlavor}",
"$formHostValue/$formHostSymbol",
preferencesRepository.installationId,
lastError
)
)
} }
} }

View File

@ -1,11 +1,23 @@
package io.github.wulkanowy.ui.modules.login.form package io.github.wulkanowy.ui.modules.login.form
import androidx.core.net.toUri import androidx.core.net.toUri
import io.github.wulkanowy.data.* import io.github.wulkanowy.data.db.entities.AdminMessage
import io.github.wulkanowy.data.enums.MessageType
import io.github.wulkanowy.data.flatResourceFlow
import io.github.wulkanowy.data.logResourceStatus
import io.github.wulkanowy.data.onResourceData
import io.github.wulkanowy.data.onResourceError
import io.github.wulkanowy.data.onResourceLoading
import io.github.wulkanowy.data.onResourceNotLoading
import io.github.wulkanowy.data.onResourceSuccess
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.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.base.BasePresenter
import io.github.wulkanowy.ui.modules.login.LoginData import io.github.wulkanowy.ui.modules.login.LoginData
import io.github.wulkanowy.ui.modules.login.LoginErrorHandler import io.github.wulkanowy.ui.modules.login.LoginErrorHandler
import io.github.wulkanowy.ui.modules.login.support.LoginSupportInfo
import io.github.wulkanowy.utils.AnalyticsHelper import io.github.wulkanowy.utils.AnalyticsHelper
import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.ifNullOrBlank import io.github.wulkanowy.utils.ifNullOrBlank
@ -17,7 +29,9 @@ class LoginFormPresenter @Inject constructor(
studentRepository: StudentRepository, studentRepository: StudentRepository,
private val loginErrorHandler: LoginErrorHandler, private val loginErrorHandler: LoginErrorHandler,
private val appInfo: AppInfo, private val appInfo: AppInfo,
private val analytics: AnalyticsHelper private val analytics: AnalyticsHelper,
private val getAppropriateAdminMessageUseCase: GetAppropriateAdminMessageUseCase,
private val preferencesRepository: PreferencesRepository,
) : BasePresenter<LoginFormView>(loginErrorHandler, studentRepository) { ) : BasePresenter<LoginFormView>(loginErrorHandler, studentRepository) {
private var lastError: Throwable? = null private var lastError: Throwable? = null
@ -36,6 +50,31 @@ class LoginFormPresenter @Inject constructor(
Timber.i("Entered wrong username or password") 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() { fun onPrivacyLinkClick() {
@ -56,7 +95,15 @@ class LoginFormPresenter @Inject constructor(
} else if (formUsernameValue == "jan@fakelog.cf" && formPassValue == "jan123") { } else if (formUsernameValue == "jan@fakelog.cf" && formPassValue == "jan123") {
setCredentials("", "") setCredentials("", "")
} }
updateCustomDomainSuffixVisibility()
updateUsernameLabel() updateUsernameLabel()
reloadAdminMessage()
}
}
fun updateCustomDomainSuffixVisibility() {
view?.run {
showDomainSuffixInput("customSuffix" in formHostValue)
} }
} }
@ -87,20 +134,36 @@ class LoginFormPresenter @Inject constructor(
} }
} }
fun onSignInClick() { private fun getLoginData(): LoginData {
val email = view?.formUsernameValue.orEmpty().trim() val email = view?.formUsernameValue.orEmpty().trim()
val password = view?.formPassValue.orEmpty().trim() val password = view?.formPassValue.orEmpty().trim()
val host = view?.formHostValue.orEmpty().trim() val host = view?.formHostValue.orEmpty().trim()
val domainSuffix = view?.formDomainSuffix.orEmpty().trim().takeIf {
"customSuffix" in host
}.orEmpty()
val symbol = view?.formHostSymbol.orEmpty().trim() val symbol = view?.formHostSymbol.orEmpty().trim()
if (!validateCredentials(email, password, host)) return return LoginData(
login = email,
password = password,
baseUrl = host,
domainSuffix = domainSuffix,
symbol = symbol
)
}
fun onSignInClick() {
val loginData = getLoginData()
if (!validateCredentials(loginData.login, loginData.password, loginData.baseUrl)) return
resourceFlow { resourceFlow {
studentRepository.getUserSubjectsFromScrapper( studentRepository.getUserSubjectsFromScrapper(
email = email, email = loginData.login,
password = password, password = loginData.password,
scrapperBaseUrl = host, scrapperBaseUrl = loginData.baseUrl,
symbol = symbol domainSuffix = loginData.domainSuffix,
symbol = loginData.symbol.orEmpty(),
) )
} }
.logResourceStatus("login") .logResourceStatus("login")
@ -112,7 +175,6 @@ class LoginFormPresenter @Inject constructor(
} }
} }
.onResourceSuccess { .onResourceSuccess {
val loginData = LoginData(email, password, host, symbol)
when (it.symbols.size) { when (it.symbols.size) {
0 -> view?.navigateToSymbol(loginData) 0 -> view?.navigateToSymbol(loginData)
else -> view?.navigateToStudentSelect(loginData, it) else -> view?.navigateToStudentSelect(loginData, it)
@ -120,7 +182,7 @@ class LoginFormPresenter @Inject constructor(
analytics.logEvent( analytics.logEvent(
"registration_form", "registration_form",
"success" to true, "success" to true,
"scrapperBaseUrl" to host, "scrapperBaseUrl" to loginData.baseUrl,
"error" to "No error" "error" to "No error"
) )
} }
@ -137,7 +199,7 @@ class LoginFormPresenter @Inject constructor(
analytics.logEvent( analytics.logEvent(
"registration_form", "registration_form",
"success" to false, "success" to false,
"scrapperBaseUrl" to host, "scrapperBaseUrl" to loginData.baseUrl,
"error" to it.message.ifNullOrBlank { "No message" } "error" to it.message.ifNullOrBlank { "No message" }
) )
} }
@ -149,7 +211,14 @@ class LoginFormPresenter @Inject constructor(
} }
fun onEmailClick() { fun onEmailClick() {
view?.openEmail(lastError?.message.ifNullOrBlank { "none" }) view?.openEmail(
LoginSupportInfo(
loginData = getLoginData(),
lastErrorMessage = lastError?.message,
registerUser = null,
enteredSymbol = null,
)
)
} }
fun onRecoverClick() { fun onRecoverClick() {

View File

@ -1,8 +1,10 @@
package io.github.wulkanowy.ui.modules.login.form 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.data.pojos.RegisterUser
import io.github.wulkanowy.ui.base.BaseView import io.github.wulkanowy.ui.base.BaseView
import io.github.wulkanowy.ui.modules.login.LoginData import io.github.wulkanowy.ui.modules.login.LoginData
import io.github.wulkanowy.ui.modules.login.support.LoginSupportInfo
interface LoginFormView : BaseView { interface LoginFormView : BaseView {
@ -14,6 +16,8 @@ interface LoginFormView : BaseView {
val formHostValue: String val formHostValue: String
val formDomainSuffix: String
val formHostSymbol: String val formHostSymbol: String
val nicknameLabel: String val nicknameLabel: String
@ -56,6 +60,12 @@ interface LoginFormView : BaseView {
fun showContent(show: Boolean) fun showContent(show: Boolean)
fun showAdminMessage(message: AdminMessage?)
fun openInternetBrowser(url: String)
fun showDomainSuffixInput(show: Boolean)
fun showOtherOptionsButton(show: Boolean) fun showOtherOptionsButton(show: Boolean)
fun showVersion() fun showVersion()
@ -70,7 +80,7 @@ interface LoginFormView : BaseView {
fun openFaqPage() fun openFaqPage()
fun openEmail(lastError: String) fun openEmail(supportInfo: LoginSupportInfo)
fun openAdvancedLogin() fun openAdvancedLogin()

View File

@ -46,7 +46,7 @@ class LoginRecoverPresenter @Inject constructor(
fun updateFields() { fun updateFields() {
view?.run { view?.run {
setUsernameHint(if ("standard" in recoverHostValue) emailHintString else loginPeselEmailHintString) setUsernameHint(if ("email" in recoverHostValue) emailHintString else loginPeselEmailHintString)
} }
} }
@ -92,7 +92,7 @@ class LoginRecoverPresenter @Inject constructor(
isCorrect = false isCorrect = false
} }
if ("standard" in host && "@" !in username) { if ("email" in host && "@" !in username) {
view?.setUsernameError(view?.invalidEmailString.orEmpty()) view?.setUsernameError(view?.invalidEmailString.orEmpty())
isCorrect = false isCorrect = false
} }

View File

@ -10,8 +10,11 @@ import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.databinding.FragmentLoginStudentSelectBinding import io.github.wulkanowy.databinding.FragmentLoginStudentSelectBinding
import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.auth.AuthDialog
import io.github.wulkanowy.ui.modules.login.LoginActivity import io.github.wulkanowy.ui.modules.login.LoginActivity
import io.github.wulkanowy.ui.modules.login.LoginData import io.github.wulkanowy.ui.modules.login.LoginData
import io.github.wulkanowy.ui.modules.login.support.LoginSupportDialog
import io.github.wulkanowy.ui.modules.login.support.LoginSupportInfo
import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.openEmailClient import io.github.wulkanowy.utils.openEmailClient
import io.github.wulkanowy.utils.openInternetBrowser import io.github.wulkanowy.utils.openInternetBrowser
@ -106,21 +109,8 @@ class LoginStudentSelectFragment :
context?.openInternetBrowser("https://discord.gg/vccAQBr", ::showMessage) context?.openInternetBrowser("https://discord.gg/vccAQBr", ::showMessage)
} }
override fun openEmail(lastError: String) { override fun openEmail(supportInfo: LoginSupportInfo) {
context?.openEmailClient( LoginSupportDialog.newInstance(supportInfo).show(childFragmentManager, "support_dialog")
chooserTitle = requireContext().getString(R.string.login_email_intent_title),
email = "wulkanowyinc@gmail.com",
subject = requireContext().getString(R.string.login_email_subject),
body = requireContext().getString(
R.string.login_email_text,
"${appInfo.systemManufacturer} ${appInfo.systemModel}",
appInfo.systemVersion.toString(),
"${appInfo.versionName}-${appInfo.buildFlavor}",
"Select users to log in",
preferencesRepository.installationId,
lastError
)
)
} }
override fun onDestroyView() { override fun onDestroyView() {

View File

@ -9,23 +9,25 @@ import io.github.wulkanowy.data.pojos.RegisterStudent
import io.github.wulkanowy.data.pojos.RegisterSymbol import io.github.wulkanowy.data.pojos.RegisterSymbol
import io.github.wulkanowy.data.pojos.RegisterUnit import io.github.wulkanowy.data.pojos.RegisterUnit
import io.github.wulkanowy.data.pojos.RegisterUser import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.data.repositories.SchoolsRepository
import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.resourceFlow import io.github.wulkanowy.data.resourceFlow
import io.github.wulkanowy.sdk.scrapper.login.AccountPermissionException
import io.github.wulkanowy.sdk.scrapper.login.InvalidSymbolException import io.github.wulkanowy.sdk.scrapper.login.InvalidSymbolException
import io.github.wulkanowy.services.sync.SyncManager import io.github.wulkanowy.services.sync.SyncManager
import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.modules.login.LoginData import io.github.wulkanowy.ui.modules.login.LoginData
import io.github.wulkanowy.ui.modules.login.LoginErrorHandler import io.github.wulkanowy.ui.modules.login.LoginErrorHandler
import io.github.wulkanowy.ui.modules.login.support.LoginSupportInfo
import io.github.wulkanowy.utils.AnalyticsHelper import io.github.wulkanowy.utils.AnalyticsHelper
import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.ifNullOrBlank import io.github.wulkanowy.utils.isCurrent
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
class LoginStudentSelectPresenter @Inject constructor( class LoginStudentSelectPresenter @Inject constructor(
studentRepository: StudentRepository, studentRepository: StudentRepository,
private val schoolsRepository: SchoolsRepository,
private val loginErrorHandler: LoginErrorHandler, private val loginErrorHandler: LoginErrorHandler,
private val syncManager: SyncManager, private val syncManager: SyncManager,
private val analytics: AnalyticsHelper, private val analytics: AnalyticsHelper,
@ -71,7 +73,14 @@ class LoginStudentSelectPresenter @Inject constructor(
students = it.dataOrNull.orEmpty() students = it.dataOrNull.orEmpty()
when (it) { when (it) {
is Resource.Loading -> Timber.d("Login student select students load started") is Resource.Loading -> Timber.d("Login student select students load started")
is Resource.Success -> refreshItems() is Resource.Success -> {
getStudentsWithCurrentlyActiveSemesters()
selectedStudents.clear()
selectedStudents.addAll(getStudentsWithCurrentlyActiveSemesters())
view?.enableSignIn(selectedStudents.isNotEmpty())
refreshItems()
}
is Resource.Error -> { is Resource.Error -> {
errorHandler.dispatch(it.error) errorHandler.dispatch(it.error)
lastError = it.error lastError = it.error
@ -81,6 +90,21 @@ class LoginStudentSelectPresenter @Inject constructor(
}.launch() }.launch()
} }
private fun getStudentsWithCurrentlyActiveSemesters(): List<LoginStudentSelectItem.Student> {
val students = registerUser.symbols.flatMap { symbol ->
symbol.schools.flatMap { unit ->
unit.students.map {
createStudentItem(it, symbol, unit, students)
}
}
}
return students.filter { student ->
student.student.semesters.any { semester ->
semester.isCurrent()
}
}
}
private fun createItems(): List<LoginStudentSelectItem> = buildList { private fun createItems(): List<LoginStudentSelectItem> = buildList {
val notEmptySymbols = registerUser.symbols.filter { it.schools.isNotEmpty() } val notEmptySymbols = registerUser.symbols.filter { it.schools.isNotEmpty() }
val emptySymbols = registerUser.symbols.filter { it.schools.isEmpty() } val emptySymbols = registerUser.symbols.filter { it.schools.isEmpty() }
@ -236,16 +260,20 @@ class LoginStudentSelectPresenter @Inject constructor(
} }
private fun registerStudents(students: List<LoginStudentSelectItem>) { private fun registerStudents(students: List<LoginStudentSelectItem>) {
val studentsWithSemesters = students val filteredStudents = students.filterIsInstance<LoginStudentSelectItem.Student>()
.filterIsInstance<LoginStudentSelectItem.Student>().map { item -> val studentsWithSemesters = filteredStudents.map { item ->
item.student.mapToStudentWithSemesters( item.student.mapToStudentWithSemesters(
user = registerUser, user = registerUser,
symbol = item.symbol, symbol = item.symbol,
unit = item.unit, scrapperDomainSuffix = loginData.domainSuffix,
colors = appInfo.defaultColorsForAvatar, unit = item.unit,
) colors = appInfo.defaultColorsForAvatar,
} )
resourceFlow { studentRepository.saveStudents(studentsWithSemesters) } }
resourceFlow {
studentRepository.saveStudents(studentsWithSemesters)
schoolsRepository.logSchoolLogin(loginData, studentsWithSemesters)
}
.logResourceStatus("registration") .logResourceStatus("registration")
.onEach { .onEach {
when (it) { when (it) {
@ -253,11 +281,13 @@ class LoginStudentSelectPresenter @Inject constructor(
showProgress(true) showProgress(true)
showContent(false) showContent(false)
} }
is Resource.Success -> { is Resource.Success -> {
syncManager.startOneTimeSyncWorker(quiet = true) syncManager.startOneTimeSyncWorker(quiet = true)
view?.navigateToNext() view?.navigateToNext()
logRegisterEvent(studentsWithSemesters) logRegisterEvent(studentsWithSemesters)
} }
is Resource.Error -> { is Resource.Error -> {
view?.apply { view?.apply {
showProgress(false) showProgress(false)
@ -280,28 +310,14 @@ class LoginStudentSelectPresenter @Inject constructor(
} }
private fun onEmailClick() { private fun onEmailClick() {
view?.openEmail(lastError?.message.ifNullOrBlank { view?.openEmail(
loginData.baseUrl + "/" + loginData.symbol + "\n" + registerUser.symbols.filterNot { LoginSupportInfo(
(it.error is AccountPermissionException || it.error is InvalidSymbolException) && it.symbol != loginData.symbol loginData = loginData,
}.joinToString(";\n") { symbol -> registerUser = registerUser,
buildString { lastErrorMessage = lastError?.message,
append(" -") enteredSymbol = loginData.symbol,
append(symbol.symbol) )
append("(${symbol.error?.message?.let { it.take(46) + "..." } ?: symbol.schools.size})") )
if (symbol.schools.isNotEmpty()) {
append(": ")
}
append(symbol.schools.joinToString(", ") { unit ->
buildString {
append(unit.schoolShortName)
append("(${unit.error?.message?.let { it.take(46) + "..." } ?: unit.students.size})")
}
})
}
} + "\nPozostałe: " + registerUser.symbols.filter {
it.error is AccountPermissionException || it.error is InvalidSymbolException
}.joinToString(", ") { it.symbol }
})
} }
private fun logRegisterEvent( private fun logRegisterEvent(

View File

@ -2,6 +2,7 @@ package io.github.wulkanowy.ui.modules.login.studentselect
import io.github.wulkanowy.ui.base.BaseView import io.github.wulkanowy.ui.base.BaseView
import io.github.wulkanowy.ui.modules.login.LoginData import io.github.wulkanowy.ui.modules.login.LoginData
import io.github.wulkanowy.ui.modules.login.support.LoginSupportInfo
interface LoginStudentSelectView : BaseView { interface LoginStudentSelectView : BaseView {
@ -23,5 +24,5 @@ interface LoginStudentSelectView : BaseView {
fun openDiscordInvite() fun openDiscordInvite()
fun openEmail(lastError: String) fun openEmail(supportInfo: LoginSupportInfo)
} }

View File

@ -0,0 +1,133 @@
package io.github.wulkanowy.ui.modules.login.support
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.core.widget.doOnTextChanged
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.databinding.DialogLoginSupportBinding
import io.github.wulkanowy.sdk.scrapper.login.AccountPermissionException
import io.github.wulkanowy.sdk.scrapper.login.InvalidSymbolException
import io.github.wulkanowy.ui.base.BaseDialogFragment
import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.openEmailClient
import io.github.wulkanowy.utils.serializable
import javax.inject.Inject
@AndroidEntryPoint
class LoginSupportDialog : BaseDialogFragment<DialogLoginSupportBinding>() {
@Inject
lateinit var appInfo: AppInfo
@Inject
lateinit var preferencesRepository: PreferencesRepository
private lateinit var supportInfo: LoginSupportInfo
companion object {
private const val ARGUMENT_KEY = "info"
fun newInstance(info: LoginSupportInfo) = LoginSupportDialog().apply {
arguments = bundleOf(ARGUMENT_KEY to info)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NO_FRAME, R.style.WulkanowyTheme_NoActionBar)
supportInfo = requireArguments().serializable(ARGUMENT_KEY)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val binding = DialogLoginSupportBinding.inflate(inflater)
.apply { binding = this }
binding.dialogLoginSupportToolbar.setNavigationOnClickListener { dismiss() }
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
with(binding) {
dialogLoginSupportSchoolInput.doOnTextChanged { _, _, _, _ ->
with(dialogLoginSupportSchoolLayout) {
isErrorEnabled = false
error = null
}
}
dialogLoginSupportSubmit.setOnClickListener {
if (dialogLoginSupportSchoolInput.text.isNullOrBlank()) {
with(dialogLoginSupportSchoolLayout) {
isErrorEnabled = true
error = getString(R.string.error_field_required)
}
} else {
onSubmitClick()
dismiss()
}
}
}
}
private fun onSubmitClick() {
with(binding) {
context?.openEmailClient(
chooserTitle = requireContext().getString(R.string.login_email_intent_title),
email = "wulkanowyinc@gmail.com",
subject = requireContext().getString(R.string.login_email_subject),
body = requireContext().getString(
R.string.login_email_text,
"${appInfo.systemManufacturer} ${appInfo.systemModel}",
appInfo.systemVersion.toString(),
"${appInfo.versionName}-${appInfo.buildFlavor}",
supportInfo.loginData.baseUrl + "/" + supportInfo.loginData.symbol,
preferencesRepository.installationId,
getLastErrorFromStudentSelectScreen(),
dialogLoginSupportSchoolInput.text.takeIf { !it.isNullOrBlank() }
?: return@with,
dialogLoginSupportAdditionalInput.text,
)
)
}
}
private fun getLastErrorFromStudentSelectScreen(): String {
if (!supportInfo.lastErrorMessage.isNullOrBlank()) {
return supportInfo.lastErrorMessage!!
}
if (supportInfo.registerUser?.symbols.isNullOrEmpty()) {
return ""
}
return "\n" + supportInfo.registerUser?.symbols?.filterNot {
(it.error is AccountPermissionException || it.error is InvalidSymbolException) &&
it.symbol != supportInfo.enteredSymbol
}?.joinToString(";\n") { symbol ->
buildString {
append(" -")
append(symbol.symbol)
append("(${symbol.error?.message?.let { it.take(46) + "..." } ?: symbol.schools.size})")
if (symbol.schools.isNotEmpty()) {
append(": ")
}
append(symbol.schools.joinToString(", ") { unit ->
buildString {
append(unit.schoolShortName)
append("(${unit.error?.message?.let { it.take(46) + "..." } ?: unit.students.size})")
}
})
}
} + "\nPozostałe: " + supportInfo.registerUser?.symbols?.filter {
it.error is AccountPermissionException || it.error is InvalidSymbolException
}?.joinToString(", ") { it.symbol }
}
}

View File

@ -0,0 +1,12 @@
package io.github.wulkanowy.ui.modules.login.support
import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.ui.modules.login.LoginData
import java.io.Serializable
data class LoginSupportInfo(
val loginData: LoginData,
val registerUser: RegisterUser?,
val lastErrorMessage: String?,
val enteredSymbol: String?,
) : Serializable

View File

@ -18,7 +18,13 @@ import io.github.wulkanowy.databinding.FragmentLoginSymbolBinding
import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.login.LoginActivity import io.github.wulkanowy.ui.modules.login.LoginActivity
import io.github.wulkanowy.ui.modules.login.LoginData import io.github.wulkanowy.ui.modules.login.LoginData
import io.github.wulkanowy.utils.* import io.github.wulkanowy.ui.modules.login.support.LoginSupportDialog
import io.github.wulkanowy.ui.modules.login.support.LoginSupportInfo
import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.hideSoftInput
import io.github.wulkanowy.utils.openInternetBrowser
import io.github.wulkanowy.utils.serializable
import io.github.wulkanowy.utils.showSoftInput
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
@ -100,6 +106,13 @@ class LoginSymbolFragment :
} }
} }
override fun setErrorSymbolDefinitelyInvalid() {
with(binding.loginSymbolNameLayout) {
requestFocus()
error = getString(R.string.login_invalid_symbol_definitely)
}
}
override fun setErrorSymbolRequire() { override fun setErrorSymbolRequire() {
setErrorSymbol(getString(R.string.error_field_required)) setErrorSymbol(getString(R.string.error_field_required))
} }
@ -163,20 +176,7 @@ class LoginSymbolFragment :
) )
} }
override fun openEmail(host: String, lastError: String) { override fun openSupportDialog(supportInfo: LoginSupportInfo) {
context?.openEmailClient( LoginSupportDialog.newInstance(supportInfo).show(childFragmentManager, "support_dialog")
chooserTitle = requireContext().getString(R.string.login_email_intent_title),
email = "wulkanowyinc@gmail.com",
subject = requireContext().getString(R.string.login_email_subject),
body = requireContext().getString(
R.string.login_email_text,
"${appInfo.systemManufacturer} ${appInfo.systemModel}",
appInfo.systemVersion.toString(),
"${appInfo.versionName}-${appInfo.buildFlavor}",
"$host/${binding.loginSymbolName.text}",
preferencesRepository.installationId,
lastError
)
)
} }
} }

View File

@ -11,6 +11,7 @@ import io.github.wulkanowy.sdk.scrapper.login.InvalidSymbolException
import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.modules.login.LoginData import io.github.wulkanowy.ui.modules.login.LoginData
import io.github.wulkanowy.ui.modules.login.LoginErrorHandler import io.github.wulkanowy.ui.modules.login.LoginErrorHandler
import io.github.wulkanowy.ui.modules.login.support.LoginSupportInfo
import io.github.wulkanowy.utils.AnalyticsHelper import io.github.wulkanowy.utils.AnalyticsHelper
import io.github.wulkanowy.utils.ifNullOrBlank import io.github.wulkanowy.utils.ifNullOrBlank
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
@ -53,6 +54,10 @@ class LoginSymbolPresenter @Inject constructor(
view?.setErrorSymbolRequire() view?.setErrorSymbolRequire()
return return
} }
if (isFormDefinitelyInvalid()) {
view?.setErrorSymbolDefinitelyInvalid()
return
}
loginData = loginData.copy( loginData = loginData.copy(
symbol = view?.symbolValue?.getNormalizedSymbol(), symbol = view?.symbolValue?.getNormalizedSymbol(),
@ -62,6 +67,7 @@ class LoginSymbolPresenter @Inject constructor(
email = loginData.login, email = loginData.login,
password = loginData.password, password = loginData.password,
scrapperBaseUrl = loginData.baseUrl, scrapperBaseUrl = loginData.baseUrl,
domainSuffix = loginData.domainSuffix,
symbol = loginData.symbol.orEmpty(), symbol = loginData.symbol.orEmpty(),
) )
}.onEach { user -> }.onEach { user ->
@ -73,6 +79,7 @@ class LoginSymbolPresenter @Inject constructor(
showProgress(true) showProgress(true)
showContent(false) showContent(false)
} }
is Resource.Success -> { is Resource.Success -> {
when (user.data.symbols.size) { when (user.data.symbols.size) {
0 -> { 0 -> {
@ -82,6 +89,7 @@ class LoginSymbolPresenter @Inject constructor(
showContact(true) showContact(true)
} }
} }
else -> { else -> {
val enteredSymbolDetails = user.data.symbols val enteredSymbolDetails = user.data.symbols
.firstOrNull() .firstOrNull()
@ -106,6 +114,7 @@ class LoginSymbolPresenter @Inject constructor(
"error" to "No error" "error" to "No error"
) )
} }
is Resource.Error -> { is Resource.Error -> {
Timber.i("Login with symbol result: An exception occurred") Timber.i("Login with symbol result: An exception occurred")
analytics.logEvent( analytics.logEvent(
@ -129,17 +138,25 @@ class LoginSymbolPresenter @Inject constructor(
}.launch("login") }.launch("login")
} }
private fun isFormDefinitelyInvalid(): Boolean {
val definitelyInvalidSymbols = listOf("vulcan", "uonet", "wulkanowy", "standardowa")
val normalizedSymbol = view?.symbolValue.orEmpty().getNormalizedSymbol()
return normalizedSymbol in definitelyInvalidSymbols
}
fun onFaqClick() { fun onFaqClick() {
view?.openFaqPage() view?.openFaqPage()
} }
fun onEmailClick() { fun onEmailClick() {
view?.openEmail(loginData.baseUrl, lastError?.message.ifNullOrBlank { view?.openSupportDialog(
registerUser?.symbols?.flatMap { symbol -> LoginSupportInfo(
symbol.schools.map { it.error?.message } + symbol.error?.message loginData = loginData,
}?.filterNotNull()?.distinct()?.joinToString(";") { registerUser = registerUser,
it.take(46) + "..." lastErrorMessage = lastError?.message,
} ?: "blank" enteredSymbol = view?.symbolValue,
}) )
)
} }
} }

View File

@ -3,6 +3,7 @@ package io.github.wulkanowy.ui.modules.login.symbol
import io.github.wulkanowy.data.pojos.RegisterUser import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.ui.base.BaseView import io.github.wulkanowy.ui.base.BaseView
import io.github.wulkanowy.ui.modules.login.LoginData import io.github.wulkanowy.ui.modules.login.LoginData
import io.github.wulkanowy.ui.modules.login.support.LoginSupportInfo
interface LoginSymbolView : BaseView { interface LoginSymbolView : BaseView {
@ -18,6 +19,8 @@ interface LoginSymbolView : BaseView {
fun setErrorSymbolInvalid() fun setErrorSymbolInvalid()
fun setErrorSymbolDefinitelyInvalid()
fun setErrorSymbolRequire() fun setErrorSymbolRequire()
fun setErrorSymbol(message: String) fun setErrorSymbol(message: String)
@ -40,5 +43,5 @@ interface LoginSymbolView : BaseView {
fun openFaqPage() fun openFaqPage()
fun openEmail(host: String, lastError: String) fun openSupportDialog(supportInfo: LoginSupportInfo)
} }

View File

@ -119,7 +119,6 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
//https://developer.android.com/guide/playcore/in-app-updates#status_callback //https://developer.android.com/guide/playcore/in-app-updates#status_callback
@Deprecated("Deprecated in Java") @Deprecated("Deprecated in Java")
@Suppress("DEPRECATION")
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data) super.onActivityResult(requestCode, resultCode, data)
updateHelper.onActivityResult(requestCode, resultCode) updateHelper.onActivityResult(requestCode, resultCode)

View File

@ -11,7 +11,9 @@ import androidx.core.view.updateMargins
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R 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.databinding.FragmentMessageBinding
import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.base.BaseFragmentPagerAdapter import io.github.wulkanowy.ui.base.BaseFragmentPagerAdapter
@ -49,6 +51,7 @@ class MessageFragment : BaseFragment<FragmentMessageBinding>(R.layout.fragment_m
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
binding = FragmentMessageBinding.bind(view) binding = FragmentMessageBinding.bind(view)
messageContainer = binding.messageViewPager
presenter.onAttachView(this) presenter.onAttachView(this)
} }
@ -95,6 +98,10 @@ class MessageFragment : BaseFragment<FragmentMessageBinding>(R.layout.fragment_m
binding.messageProgress.visibility = if (show) VISIBLE else INVISIBLE binding.messageProgress.visibility = if (show) VISIBLE else INVISIBLE
} }
override fun showMessage(messageId: Int) {
showMessage(getString(messageId))
}
override fun showNewMessage(show: Boolean) { override fun showNewMessage(show: Boolean) {
binding.openSendMessageButton.run { binding.openSendMessageButton.run {
if (show) show() else hide() if (show) show() else hide()

View File

@ -1,5 +1,7 @@
package io.github.wulkanowy.ui.modules.message 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.data.repositories.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.ui.base.ErrorHandler
@ -9,7 +11,8 @@ import javax.inject.Inject
class MessagePresenter @Inject constructor( class MessagePresenter @Inject constructor(
errorHandler: ErrorHandler, errorHandler: ErrorHandler,
studentRepository: StudentRepository studentRepository: StudentRepository,
private val preferencesRepository: PreferencesRepository,
) : BasePresenter<MessageView>(errorHandler, studentRepository) { ) : BasePresenter<MessageView>(errorHandler, studentRepository) {
override fun onAttachView(view: MessageView) { override fun onAttachView(view: MessageView) {
@ -19,6 +22,14 @@ class MessagePresenter @Inject constructor(
Timber.i("Message view was initialized") Timber.i("Message view was initialized")
loadData() loadData()
} }
showIncognitoModeReminderMessage()
}
private fun showIncognitoModeReminderMessage() {
if (preferencesRepository.isIncognitoMode) {
view?.showMessage(R.string.message_incognito_mode_on)
}
} }
fun onPageSelected(index: Int) { fun onPageSelected(index: Int) {

View File

@ -1,5 +1,6 @@
package io.github.wulkanowy.ui.modules.message package io.github.wulkanowy.ui.modules.message
import androidx.annotation.StringRes
import io.github.wulkanowy.ui.base.BaseView import io.github.wulkanowy.ui.base.BaseView
interface MessageView : BaseView { interface MessageView : BaseView {
@ -12,6 +13,8 @@ interface MessageView : BaseView {
fun showProgress(show: Boolean) fun showProgress(show: Boolean)
fun showMessage(@StringRes messageId: Int)
fun showNewMessage(show: Boolean) fun showNewMessage(show: Boolean)
fun showTabLayout(show: Boolean) fun showTabLayout(show: Boolean)

View File

@ -12,6 +12,7 @@ import android.view.View.VISIBLE
import android.webkit.WebResourceRequest import android.webkit.WebResourceRequest
import android.webkit.WebView import android.webkit.WebView
import android.webkit.WebViewClient import android.webkit.WebViewClient
import androidx.annotation.StringRes
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
@ -164,6 +165,10 @@ class MessagePreviewFragment :
binding.messagePreviewErrorRetry.setOnClickListener { callback() } binding.messagePreviewErrorRetry.setOnClickListener { callback() }
} }
override fun showMessage(@StringRes messageId: Int) {
showMessage(getString(messageId))
}
override fun openMessageReply(message: Message?) { override fun openMessageReply(message: Message?) {
context?.let { it.startActivity(SendMessageActivity.getStartIntent(it, message, true)) } context?.let { it.startActivity(SendMessageActivity.getStartIntent(it, message, true)) }
} }

View File

@ -2,11 +2,13 @@ package io.github.wulkanowy.ui.modules.message.preview
import android.annotation.SuppressLint import android.annotation.SuppressLint
import androidx.core.text.parseAsHtml import androidx.core.text.parseAsHtml
import io.github.wulkanowy.R
import io.github.wulkanowy.data.* import io.github.wulkanowy.data.*
import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.MessageAttachment import io.github.wulkanowy.data.db.entities.MessageAttachment
import io.github.wulkanowy.data.enums.MessageFolder import io.github.wulkanowy.data.enums.MessageFolder
import io.github.wulkanowy.data.repositories.MessageRepository 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.data.repositories.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.ui.base.ErrorHandler
@ -20,6 +22,7 @@ class MessagePreviewPresenter @Inject constructor(
errorHandler: ErrorHandler, errorHandler: ErrorHandler,
studentRepository: StudentRepository, studentRepository: StudentRepository,
private val messageRepository: MessageRepository, private val messageRepository: MessageRepository,
private val preferencesRepository: PreferencesRepository,
private val analytics: AnalyticsHelper private val analytics: AnalyticsHelper
) : BasePresenter<MessagePreviewView>(errorHandler, studentRepository) { ) : BasePresenter<MessagePreviewView>(errorHandler, studentRepository) {
@ -54,7 +57,11 @@ class MessagePreviewPresenter @Inject constructor(
private fun loadData(messageToLoad: Message) { private fun loadData(messageToLoad: Message) {
flatResourceFlow { flatResourceFlow {
val student = studentRepository.getCurrentStudent() val student = studentRepository.getCurrentStudent()
messageRepository.getMessage(student, messageToLoad, true) messageRepository.getMessage(
student = student,
message = messageToLoad,
markAsRead = !preferencesRepository.isIncognitoMode,
)
} }
.logResourceStatus("message ${messageToLoad.messageId} preview") .logResourceStatus("message ${messageToLoad.messageId} preview")
.onResourceData { .onResourceData {
@ -65,6 +72,10 @@ class MessagePreviewPresenter @Inject constructor(
setMessageWithAttachment(it) setMessageWithAttachment(it)
showContent(true) showContent(true)
initOptions() initOptions()
if (preferencesRepository.isIncognitoMode && it.message.unread) {
showMessage(R.string.message_incognito_description)
}
} }
} else { } else {
view?.run { view?.run {

View File

@ -1,5 +1,6 @@
package io.github.wulkanowy.ui.modules.message.preview 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.Message
import io.github.wulkanowy.data.db.entities.MessageWithAttachment import io.github.wulkanowy.data.db.entities.MessageWithAttachment
import io.github.wulkanowy.ui.base.BaseView import io.github.wulkanowy.ui.base.BaseView
@ -43,4 +44,6 @@ interface MessagePreviewView : BaseView {
fun popView() fun popView()
fun printDocument(html: String, jobName: String) fun printDocument(html: String, jobName: String)
fun showMessage(@StringRes messageId: Int)
} }

View File

@ -7,6 +7,7 @@ import android.view.MenuItem
import android.view.View import android.view.View
import android.view.View.* import android.view.View.*
import android.widget.CompoundButton import android.widget.CompoundButton
import androidx.annotation.StringRes
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
@ -134,14 +135,20 @@ class MessageTabFragment : BaseFragment<FragmentMessageTabBinding>(R.layout.frag
} }
} }
@Deprecated("Deprecated in Java")
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater) super.onCreateOptionsMenu(menu, inflater)
inflater.inflate(R.menu.action_menu_message_tab, menu) inflater.inflate(R.menu.action_menu_message_tab, menu)
val searchView = menu.findItem(R.id.action_search).actionView as SearchView initializeSearchView(menu)
searchView.queryHint = getString(R.string.all_search_hint) }
searchView.maxWidth = Int.MAX_VALUE
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 { searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String) = false override fun onQueryTextSubmit(query: String) = false
override fun onQueryTextChange(query: String): Boolean { override fun onQueryTextChange(query: String): Boolean {
@ -207,8 +214,8 @@ class MessageTabFragment : BaseFragment<FragmentMessageTabBinding>(R.layout.frag
binding.messageTabSwipe.isRefreshing = show binding.messageTabSwipe.isRefreshing = show
} }
override fun showMessagesDeleted() { override fun showMessage(@StringRes messageId: Int) {
showMessage(getString(R.string.message_messages_deleted)) showMessage(getString(messageId))
} }
override fun notifyParentShowNewMessage(show: Boolean) { override fun notifyParentShowNewMessage(show: Boolean) {

View File

@ -1,5 +1,6 @@
package io.github.wulkanowy.ui.modules.message.tab package io.github.wulkanowy.ui.modules.message.tab
import io.github.wulkanowy.R
import io.github.wulkanowy.data.* import io.github.wulkanowy.data.*
import io.github.wulkanowy.data.db.entities.Mailbox import io.github.wulkanowy.data.db.entities.Mailbox
import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.db.entities.Message
@ -26,7 +27,7 @@ class MessageTabPresenter @Inject constructor(
errorHandler: ErrorHandler, errorHandler: ErrorHandler,
studentRepository: StudentRepository, studentRepository: StudentRepository,
private val messageRepository: MessageRepository, private val messageRepository: MessageRepository,
private val analytics: AnalyticsHelper private val analytics: AnalyticsHelper,
) : BasePresenter<MessageTabView>(errorHandler, studentRepository) { ) : BasePresenter<MessageTabView>(errorHandler, studentRepository) {
lateinit var folder: MessageFolder lateinit var folder: MessageFolder
@ -135,7 +136,7 @@ class MessageTabPresenter @Inject constructor(
messageRepository.deleteMessages(student, selectedMailbox, messageList) messageRepository.deleteMessages(student, selectedMailbox, messageList)
} }
.onFailure(errorHandler::dispatch) .onFailure(errorHandler::dispatch)
.onSuccess { view?.showMessagesDeleted() } .onSuccess { view?.showMessage(R.string.message_messages_deleted) }
} }
} }

View File

@ -1,5 +1,6 @@
package io.github.wulkanowy.ui.modules.message.tab 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.Mailbox
import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.ui.base.BaseView import io.github.wulkanowy.ui.base.BaseView
@ -26,7 +27,7 @@ interface MessageTabView : BaseView {
fun showEmpty(show: Boolean) fun showEmpty(show: Boolean)
fun showMessagesDeleted() fun showMessage(@StringRes messageId: Int)
fun showErrorView(show: Boolean) fun showErrorView(show: Boolean)

View File

@ -12,7 +12,9 @@ import androidx.recyclerview.widget.RecyclerView
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Timetable import io.github.wulkanowy.data.db.entities.Timetable
import io.github.wulkanowy.databinding.ItemTimetableBinding import io.github.wulkanowy.databinding.ItemTimetableBinding
import io.github.wulkanowy.databinding.ItemTimetableEmptyBinding
import io.github.wulkanowy.databinding.ItemTimetableSmallBinding import io.github.wulkanowy.databinding.ItemTimetableSmallBinding
import io.github.wulkanowy.utils.getPlural
import io.github.wulkanowy.utils.getThemeAttrColor import io.github.wulkanowy.utils.getThemeAttrColor
import io.github.wulkanowy.utils.toFormattedString import io.github.wulkanowy.utils.toFormattedString
import javax.inject.Inject import javax.inject.Inject
@ -29,9 +31,14 @@ class TimetableAdapter @Inject constructor() :
TimetableItemType.SMALL -> SmallViewHolder( TimetableItemType.SMALL -> SmallViewHolder(
ItemTimetableSmallBinding.inflate(inflater, parent, false) ItemTimetableSmallBinding.inflate(inflater, parent, false)
) )
TimetableItemType.NORMAL -> NormalViewHolder( TimetableItemType.NORMAL -> NormalViewHolder(
ItemTimetableBinding.inflate(inflater, parent, false) ItemTimetableBinding.inflate(inflater, parent, false)
) )
TimetableItemType.EMPTY -> EmptyViewHolder(
ItemTimetableEmptyBinding.inflate(inflater, parent, false)
)
} }
} }
@ -40,12 +47,12 @@ class TimetableAdapter @Inject constructor() :
position: Int, position: Int,
payloads: MutableList<Any> payloads: MutableList<Any>
) { ) {
if (payloads.isEmpty()) return super.onBindViewHolder(holder, position, payloads) if (payloads.isNotEmpty() && holder is NormalViewHolder) {
updateTimeLeft(
if (holder is NormalViewHolder) updateTimeLeft( binding = holder.binding,
binding = holder.binding, timeLeft = (getItem(position) as TimetableItem.Normal).timeLeft,
timeLeft = (getItem(position) as TimetableItem.Normal).timeLeft, )
) } else super.onBindViewHolder(holder, position, payloads)
} }
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
@ -54,10 +61,16 @@ class TimetableAdapter @Inject constructor() :
binding = holder.binding, binding = holder.binding,
item = getItem(position) as TimetableItem.Small, item = getItem(position) as TimetableItem.Small,
) )
is NormalViewHolder -> bindNormalView( is NormalViewHolder -> bindNormalView(
binding = holder.binding, binding = holder.binding,
item = getItem(position) as TimetableItem.Normal, 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?) { private fun updateTimeLeft(binding: ItemTimetableBinding, timeLeft: TimeLeft?) {
with(binding) { with(binding) {
when { when {
@ -137,6 +163,7 @@ class TimetableAdapter @Inject constructor() :
timetableItemTimeLeft.visibility = VISIBLE timetableItemTimeLeft.visibility = VISIBLE
timetableItemTimeLeft.text = root.context.getString(R.string.timetable_finished) timetableItemTimeLeft.text = root.context.getString(R.string.timetable_finished)
} }
else -> { else -> {
timetableItemTimeUntil.visibility = GONE timetableItemTimeUntil.visibility = GONE
timetableItemTimeLeft.visibility = GONE timetableItemTimeLeft.visibility = GONE
@ -191,7 +218,8 @@ class TimetableAdapter @Inject constructor() :
) )
} else { } else {
timetableItemDescription.visibility = GONE 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() timetableItemGroup.isVisible = item.showGroupsInPlan && lesson.group.isNotBlank()
timetableItemTeacher.visibility = VISIBLE timetableItemTeacher.visibility = VISIBLE
} }
@ -274,6 +302,9 @@ class TimetableAdapter @Inject constructor() :
private class SmallViewHolder(val binding: ItemTimetableSmallBinding) : private class SmallViewHolder(val binding: ItemTimetableSmallBinding) :
RecyclerView.ViewHolder(binding.root) RecyclerView.ViewHolder(binding.root)
private class EmptyViewHolder(val binding: ItemTimetableEmptyBinding) :
RecyclerView.ViewHolder(binding.root)
companion object { companion object {
private val differ = object : DiffUtil.ItemCallback<TimetableItem>() { private val differ = object : DiffUtil.ItemCallback<TimetableItem>() {
override fun areItemsTheSame(oldItem: TimetableItem, newItem: TimetableItem): Boolean = 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 is TimetableItem.Small && newItem is TimetableItem.Small -> {
oldItem.lesson.start == newItem.lesson.start oldItem.lesson.start == newItem.lesson.start
} }
oldItem is TimetableItem.Normal && newItem is TimetableItem.Normal -> { oldItem is TimetableItem.Normal && newItem is TimetableItem.Normal -> {
oldItem.lesson.start == newItem.lesson.start oldItem.lesson.start == newItem.lesson.start
} }
else -> oldItem == newItem else -> oldItem == newItem
} }

View File

@ -16,6 +16,11 @@ sealed class TimetableItem(val type: TimetableItemType) {
val timeLeft: TimeLeft?, val timeLeft: TimeLeft?,
val onClick: (Timetable) -> Unit, val onClick: (Timetable) -> Unit,
) : TimetableItem(TimetableItemType.NORMAL) ) : TimetableItem(TimetableItemType.NORMAL)
data class Empty(
val numFrom: Int,
val numTo: Int
) : TimetableItem(TimetableItemType.EMPTY)
} }
data class TimeLeft( data class TimeLeft(
@ -27,4 +32,5 @@ data class TimeLeft(
enum class TimetableItemType { enum class TimetableItemType {
SMALL, SMALL,
NORMAL, NORMAL,
EMPTY
} }

View File

@ -1,23 +1,44 @@
package io.github.wulkanowy.ui.modules.timetable 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.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.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.PreferencesRepository
import io.github.wulkanowy.data.repositories.SemesterRepository import io.github.wulkanowy.data.repositories.SemesterRepository
import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.repositories.TimetableRepository import io.github.wulkanowy.data.repositories.TimetableRepository
import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler 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.catch
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import timber.log.Timber import timber.log.Timber
import java.time.Instant import java.time.Instant
import java.time.LocalDate import java.time.LocalDate
import java.time.LocalDate.* import java.time.LocalDate.now
import java.util.* import java.time.LocalDate.of
import java.time.LocalDate.ofEpochDay
import java.util.Timer
import javax.inject.Inject import javax.inject.Inject
import kotlin.concurrent.timer import kotlin.concurrent.timer
@ -192,16 +213,38 @@ class TimetablePresenter @Inject constructor(
compareBy({ item -> item.number }, { item -> !item.isStudentPlan }) compareBy({ item -> item.number }, { item -> !item.isStudentPlan })
) )
return filteredItems.mapIndexed { i, it -> var prevNum = when (prefRepository.showTimetableGaps) {
if (it.isStudentPlan) TimetableItem.Normal( BETWEEN_AND_BEFORE_LESSONS -> 0
lesson = it, else -> null
showGroupsInPlan = prefRepository.showGroupsInPlan, }
timeLeft = filteredItems.getTimeLeftForLesson(it, i), return buildList {
onClick = ::onTimetableItemSelected filteredItems.forEachIndexed { i, it ->
) else TimetableItem.Small( if (prefRepository.showTimetableGaps != NO_GAPS && prevNum != null && it.number > prevNum!! + 1) {
lesson = it, val emptyLesson = TimetableItem.Empty(
onClick = ::onTimetableItemSelected 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
}
} }
} }

View File

@ -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.Semester
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.Timetable 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.SemesterRepository
import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.repositories.TimetableRepository 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.getStudentWidgetKey
import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetProvider.Companion.getTodayLastLessonEndDateTimeWidgetKey import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetProvider.Companion.getTodayLastLessonEndDateTimeWidgetKey
import io.github.wulkanowy.utils.getCompatColor import io.github.wulkanowy.utils.getCompatColor
import io.github.wulkanowy.utils.getPlural
import io.github.wulkanowy.utils.toFormattedString import io.github.wulkanowy.utils.toFormattedString
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import timber.log.Timber import timber.log.Timber
@ -35,11 +39,12 @@ class TimetableWidgetFactory(
private val studentRepository: StudentRepository, private val studentRepository: StudentRepository,
private val semesterRepository: SemesterRepository, private val semesterRepository: SemesterRepository,
private val sharedPref: SharedPrefProvider, private val sharedPref: SharedPrefProvider,
private val prefRepository: PreferencesRepository,
private val context: Context, private val context: Context,
private val intent: Intent? private val intent: Intent?
) : RemoteViewsService.RemoteViewsFactory { ) : RemoteViewsService.RemoteViewsFactory {
private var lessons = emptyList<Timetable>() private var items = emptyList<TimetableWidgetItem>()
private var timetableCanceledColor: Int? = null private var timetableCanceledColor: Int? = null
@ -47,18 +52,13 @@ class TimetableWidgetFactory(
private var timetableChangeColor: Int? = null private var timetableChangeColor: Int? = null
private var lastSyncInstant: Instant? = null
override fun getLoadingView() = null override fun getLoadingView() = null
override fun hasStableIds() = true override fun hasStableIds() = true
override fun getCount() = when { override fun getCount() = items.size
lessons.isEmpty() -> 0
else -> lessons.size + 1
}
override fun getViewTypeCount() = 2 override fun getViewTypeCount() = 3
override fun getItemId(position: Int) = position.toLong() override fun getItemId(position: Int) = position.toLong()
@ -75,9 +75,10 @@ class TimetableWidgetFactory(
runBlocking { runBlocking {
val student = getStudent(studentId) ?: return@runBlocking val student = getStudent(studentId) ?: return@runBlocking
val semester = semesterRepository.getCurrentSemester(student) val semester = semesterRepository.getCurrentSemester(student)
lessons = getLessons(student, semester, date) items = createItems(
lastSyncInstant = lessons = getLessons(student, semester, date),
timetableRepository.getLastRefreshTimestamp(semester, date, date) lastSync = timetableRepository.getLastRefreshTimestamp(semester, date, date)
)
if (date == LocalDate.now()) { if (date == LocalDate.now()) {
updateTodayLastLessonEnd(appWidgetId) updateTodayLastLessonEnd(appWidgetId)
} }
@ -101,8 +102,33 @@ class TimetableWidgetFactory(
return lessons.sortedBy { it.number } return lessons.sortedBy { it.number }
} }
private fun createItems(
lessons: List<Timetable>,
lastSync: Instant?,
): List<TimetableWidgetItem> {
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) { private fun updateTodayLastLessonEnd(appWidgetId: Int) {
val todayLastLessonEnd = lessons.maxOfOrNull { it.end } ?: return val todayLastLessonEnd = items.filterIsInstance<TimetableWidgetItem.Normal>()
.maxOfOrNull { it.lesson.end } ?: return
val key = getTodayLastLessonEndDateTimeWidgetKey(appWidgetId) val key = getTodayLastLessonEndDateTimeWidgetKey(appWidgetId)
sharedPref.putLong(key, todayLastLessonEnd.epochSecond, true) sharedPref.putLong(key, todayLastLessonEnd.epochSecond, true)
} }
@ -112,44 +138,81 @@ class TimetableWidgetFactory(
} }
override fun getViewAt(position: Int): RemoteViews? { override fun getViewAt(position: Int): RemoteViews? {
if (position == lessons.size) { return when (val item = items.getOrNull(position) ?: return null) {
val synchronizationInstant = lastSyncInstant ?: Instant.MIN is TimetableWidgetItem.Normal -> getNormalItemRemoteView(item)
val synchronizationText = getSynchronizationInfoText(synchronizationInstant) is TimetableWidgetItem.Empty -> getEmptyItemRemoteView(item)
return RemoteViews(context.packageName, R.layout.item_widget_timetable_footer).apply { is TimetableWidgetItem.Synchronized -> getSynchronizedItemRemoteView(item)
setTextViewText(R.id.timetableWidgetSynchronizationTime, synchronizationText)
}
} }
}
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 lessonStartTime = lesson.start.toFormattedString(TIME_FORMAT_STYLE)
val lessonEndTime = lesson.end.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 { val remoteViews = RemoteViews(context.packageName, R.layout.item_widget_timetable).apply {
setTextViewText(R.id.timetableWidgetItemNumber, lesson.number.toString()) setTextViewText(R.id.timetableWidgetItemNumber, lesson.number.toString())
setTextViewText(R.id.timetableWidgetItemTimeStart, lessonStartTime) setTextViewText(R.id.timetableWidgetItemTimeStart, lessonStartTime)
setTextViewText(R.id.timetableWidgetItemTimeFinish, lessonEndTime) setTextViewText(R.id.timetableWidgetItemTimeFinish, lessonEndTime)
setTextViewText(R.id.timetableWidgetItemSubject, lesson.subject) setTextViewText(R.id.timetableWidgetItemSubject, lesson.subject)
setTextViewText(R.id.timetableWidgetItemRoom, roomText)
setTextViewText(R.id.timetableWidgetItemTeacher, lesson.teacher) setTextViewText(R.id.timetableWidgetItemTeacher, lesson.teacher)
setTextViewText(R.id.timetableWidgetItemDescription, lesson.info) setTextViewText(R.id.timetableWidgetItemDescription, lesson.info)
setOnClickFillInIntent(R.id.timetableWidgetItemContainer, Intent()) setOnClickFillInIntent(R.id.timetableWidgetItemContainer, Intent())
} }
updateTheme() updateTheme()
clearLessonStyles(remoteViews) clearLessonStyles(remoteViews)
if (lesson.room.isBlank()) {
remoteViews.setViewVisibility(R.id.timetableWidgetItemRoom, GONE)
} else {
remoteViews.setTextViewText(R.id.timetableWidgetItemRoom, lesson.room)
}
when { when {
lesson.canceled -> applyCancelledLessonStyles(remoteViews) lesson.canceled -> applyCancelledLessonStyles(remoteViews)
lesson.changes or lesson.info.isNotBlank() -> applyChangedLessonStyles( lesson.changes or lesson.info.isNotBlank() -> applyChangedLessonStyles(
remoteViews, lesson remoteViews = remoteViews,
lesson = lesson,
) )
} }
return remoteViews 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() { private fun updateTheme() {
when (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) { when (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) {
Configuration.UI_MODE_NIGHT_YES -> { Configuration.UI_MODE_NIGHT_YES -> {

View File

@ -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,
}

View File

@ -8,6 +8,7 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK import android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import android.graphics.Bitmap
import android.widget.RemoteViews import android.widget.RemoteViews
import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.content.res.AppCompatResources
import androidx.core.graphics.drawable.DrawableCompat import androidx.core.graphics.drawable.DrawableCompat
@ -76,110 +77,151 @@ class TimetableWidgetProvider : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
GlobalScope.launch { GlobalScope.launch {
when (intent.action) { when (intent.action) {
ACTION_APPWIDGET_UPDATE -> onUpdate(context, intent) ACTION_APPWIDGET_UPDATE -> onWidgetUpdate(context, intent)
ACTION_APPWIDGET_DELETED -> onDelete(intent) ACTION_APPWIDGET_DELETED -> onWidgetDeleted(intent)
} }
} }
} }
private suspend fun onUpdate(context: Context, intent: Intent) { private suspend fun onWidgetUpdate(context: Context, intent: Intent) {
if (intent.getStringExtra(EXTRA_BUTTON_TYPE) == null) { val pressedButton = intent.getPressedButton()
val isFromConfigure = intent.getBooleanExtra(EXTRA_FROM_CONFIGURE, false)
val appWidgetIds = intent.getIntArrayExtra(EXTRA_APPWIDGET_IDS) ?: return
appWidgetIds.forEach { appWidgetId -> if (pressedButton == null) {
val student = val updatedWidgetIds = intent.getWidgetIds() ?: return
getStudent(sharedPref.getLong(getStudentWidgetKey(appWidgetId), 0), appWidgetId) updatedWidgetIds.forEach { updateWidgetLayout(context, it) }
val savedDataEpochDay = sharedPref.getLong(getDateWidgetKey(appWidgetId), 0)
val dateToLoad = if (isFromConfigure && savedDataEpochDay != 0L) {
LocalDate.ofEpochDay(savedDataEpochDay)
} else {
getWidgetDefaultDateToLoad(appWidgetId)
}
updateWidget(context, appWidgetId, dateToLoad, student)
}
} else { } else {
val buttonType = intent.getStringExtra(EXTRA_BUTTON_TYPE) val widgetId = intent.getToggledWidgetId() ?: return
val toggledWidgetId = intent.getIntExtra(EXTRA_TOGGLED_WIDGET_ID, 0) reportChangedDay(pressedButton)
val student = getStudent( updateSavedWidgetDate(widgetId, pressedButton)
sharedPref.getLong(getStudentWidgetKey(toggledWidgetId), 0), toggledWidgetId updateWidgetLayout(context, widgetId)
)
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)
} }
} }
private fun onDelete(intent: Intent) { private fun Intent.getPressedButton(): String? {
val appWidgetId = intent.getIntExtra(EXTRA_APPWIDGET_ID, 0) return getStringExtra(EXTRA_BUTTON_TYPE)
}
if (appWidgetId != 0) { private fun Intent.getWidgetIds(): IntArray? {
with(sharedPref) { return getIntArrayExtra(EXTRA_APPWIDGET_IDS)
delete(getStudentWidgetKey(appWidgetId)) }
delete(getDateWidgetKey(appWidgetId))
} 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( private fun updateSavedWidgetDate(widgetId: Int, buttonType: String) {
context: Context, appWidgetId: Int, date: LocalDate, student: Student? 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 widgetRemoteViews = RemoteViews(context.packageName, R.layout.widget_timetable)
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
)
// 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 formattedDate = date.toFormattedString("EEE, dd.MM").capitalise()
val remoteView = RemoteViews(context.packageName, R.layout.widget_timetable).apply { widgetRemoteViews.setTextViewText(R.id.timetableWidgetDate, formattedDate)
setEmptyView(R.id.timetableWidgetList, R.id.timetableWidgetEmpty)
setTextViewText(R.id.timetableWidgetDate, formattedDate) // Apply intents to the date switcher buttons
setRemoteAdapter(R.id.timetableWidgetList, adapterIntent) 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.timetableWidgetNext, nextNavIntent)
setOnClickPendingIntent(R.id.timetableWidgetPrev, prevNavIntent) setOnClickPendingIntent(R.id.timetableWidgetPrev, prevNavIntent)
setOnClickPendingIntent(R.id.timetableWidgetDate, resetNavIntent) setOnClickPendingIntent(R.id.timetableWidgetDate, resetNavIntent)
setPendingIntentTemplate(R.id.timetableWidgetList, appIntent)
} }
student?.let { // Setup the lesson list adapter
setupAccountView(context, student, remoteView, appWidgetId) 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) { // Setup profile picture
putLong(getDateWidgetKey(appWidgetId), date.toEpochDay(), true) getWidgetStudent(widgetId)?.let { student ->
setupAccountView(context, student, widgetRemoteViews, widgetId)
} }
// Apply updates
with(appWidgetManager) { with(appWidgetManager) {
partiallyUpdateAppWidget(appWidgetId, remoteView) partiallyUpdateAppWidget(widgetId, widgetRemoteViews)
notifyAppWidgetViewDataChanged(appWidgetId, R.id.timetableWidgetList) notifyAppWidgetViewDataChanged(widgetId, R.id.timetableWidgetList)
} }
Timber.d("TimetableWidgetProvider updated") 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 context: Context, code: Int, appWidgetId: Int, buttonType: String
) = PendingIntent.getBroadcast( ) = PendingIntent.getBroadcast(
context, code, Intent(context, TimetableWidgetProvider::class.java).apply { context, code, Intent(context, TimetableWidgetProvider::class.java).apply {
@ -189,6 +231,17 @@ class TimetableWidgetProvider : BroadcastReceiver() {
}, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE }, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE
) )
private fun createLessonListAdapterIntent(context: Context, widgetId: Int) =
Intent(context, TimetableWidgetService::class.java).apply {
putExtra(EXTRA_APPWIDGET_ID, widgetId)
action = widgetId.toString() //make Intent unique
}
private suspend fun getWidgetStudent(widgetId: Int): Student? {
val studentId = sharedPref.getLong(getStudentWidgetKey(widgetId), 0)
return getStudent(studentId, widgetId)
}
private suspend fun getStudent(studentId: Long, appWidgetId: Int) = try { private suspend fun getStudent(studentId: Long, appWidgetId: Int) = try {
val students = studentRepository.getSavedStudents(false) val students = studentRepository.getSavedStudents(false)
val student = students.singleOrNull { it.student.id == studentId }?.student val student = students.singleOrNull { it.student.id == studentId }?.student
@ -199,6 +252,7 @@ class TimetableWidgetProvider : BroadcastReceiver() {
sharedPref.putLong(getStudentWidgetKey(appWidgetId), it.id) sharedPref.putLong(getStudentWidgetKey(appWidgetId), it.id)
} }
} }
else -> null else -> null
} }
} catch (e: Exception) { } catch (e: Exception) {
@ -208,60 +262,64 @@ class TimetableWidgetProvider : BroadcastReceiver() {
null null
} }
private fun getWidgetDefaultDateToLoad(appWidgetId: Int): LocalDate { private fun setupAccountView(
val lastLessonEndTimestamp = context: Context, student: Student, remoteViews: RemoteViews, widgetId: Int
sharedPref.getLong(getTodayLastLessonEndDateTimeWidgetKey(appWidgetId), 0) ) {
val lastLessonEndDateTime = val accountInitials = getAccountInitials(student.nickOrName)
LocalDateTime.ofEpochSecond(lastLessonEndTimestamp, 0, ZoneOffset.UTC) val accountPickerPendingIntent = createAccountPickerPendingIntent(context, widgetId)
val todayDate = LocalDate.now() getAvatarBackgroundBitmap(context, student.avatarColor)?.let {
val isLastLessonEndDateNow = lastLessonEndDateTime.toLocalDate() == todayDate remoteViews.setImageViewBitmap(R.id.timetableWidgetAccountBackground, it)
val isLastLessonEndDateAfterNowTime = LocalDateTime.now() > lastLessonEndDateTime }
return if (isLastLessonEndDateNow && isLastLessonEndDateAfterNowTime) { remoteViews.apply {
todayDate.nextSchoolDay setTextViewText(R.id.timetableWidgetAccountInitials, accountInitials)
} else { setOnClickPendingIntent(R.id.timetableWidgetAccount, accountPickerPendingIntent)
todayDate.nextOrSameSchoolDay
} }
} }
private fun setupAccountView( private fun getAccountInitials(name: String): String {
context: Context, val firstLetters = name.split(" ").mapNotNull { it.firstOrNull() }
student: Student, return firstLetters.joinToString(separator = "").uppercase()
remoteViews: RemoteViews, }
appWidgetId: Int
) {
val accountInitials = student.nickOrName
.split(" ")
.mapNotNull { it.firstOrNull() }.take(2)
.joinToString(separator = "").uppercase()
val accountPickerIntent = PendingIntent.getActivity( private fun createAccountPickerPendingIntent(context: Context, widgetId: Int) =
PendingIntent.getActivity(
context, context,
-Int.MAX_VALUE + appWidgetId, -Int.MAX_VALUE + widgetId,
Intent(context, TimetableWidgetConfigureActivity::class.java).apply { Intent(context, TimetableWidgetConfigureActivity::class.java).apply {
addFlags(FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_CLEAR_TASK) 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) putExtra(EXTRA_FROM_PROVIDER, true)
}, },
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE 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 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 screenDensity = context.resources.displayMetrics.density
val avatarSize = (48 * screenDensity).toInt() val avatarSize = (48 * screenDensity).toInt()
val backgroundBitmap = DrawableCompat.wrap(drawable).run { DrawableCompat.wrap(drawable).run {
DrawableCompat.setTint(this, student.avatarColor.toInt()) DrawableCompat.setTint(this, avatarColor.toInt())
toBitmap(avatarSize, avatarSize) toBitmap(avatarSize, avatarSize)
} }
remoteViews.setImageViewBitmap(R.id.timetableWidgetAccountBackground, backgroundBitmap)
} }
}
remoteViews.apply { private fun onWidgetDeleted(intent: Intent) {
setTextViewText(R.id.timetableWidgetAccountInitials, accountInitials) val deletedWidgetId = intent.getWidgetId()
setOnClickPendingIntent(R.id.timetableWidgetAccount, accountPickerIntent) 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))
} }
} }
} }

View File

@ -25,7 +25,8 @@ open class AppInfo @Inject constructor() {
open val systemModel: String get() = Build.MODEL open val systemModel: String get() = Build.MODEL
open val messagesBaseUrl = BuildConfig.MESSAGES_BASE_URL open val messagesBaseUrl: String = BuildConfig.MESSAGES_BASE_URL
open val schoolsBaseUrl: String = BuildConfig.SCHOOLS_BASE_URL
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
open val systemLanguage: String open val systemLanguage: String

View File

@ -7,7 +7,7 @@ import android.os.Bundle
import android.view.View import android.view.View
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import fr.bipi.tressence.common.filters.Filter import fr.bipi.treessence.common.filters.Filter
import io.github.wulkanowy.sdk.exception.FeatureNotAvailableException import io.github.wulkanowy.sdk.exception.FeatureNotAvailableException
import io.github.wulkanowy.sdk.scrapper.exception.FeatureDisabledException import io.github.wulkanowy.sdk.scrapper.exception.FeatureDisabledException
import timber.log.Timber import timber.log.Timber

View File

@ -16,6 +16,7 @@ fun Sdk.init(student: Student): Sdk {
mobileBaseUrl = student.mobileBaseUrl mobileBaseUrl = student.mobileBaseUrl
} else { } else {
scrapperBaseUrl = student.scrapperBaseUrl scrapperBaseUrl = student.scrapperBaseUrl
domainSuffix = student.scrapperDomainSuffix
loginType = Sdk.ScrapperLoginType.valueOf(student.loginType) loginType = Sdk.ScrapperLoginType.valueOf(student.loginType)
} }

View File

@ -1,16 +1,27 @@
package io.github.wulkanowy.utils package io.github.wulkanowy.utils
import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Semester
import java.time.LocalDate
import java.time.LocalDate.now import java.time.LocalDate.now
import java.time.Month
inline val Semester.isCurrent: Boolean fun Semester.isCurrent(now: LocalDate = now()): Boolean {
get() = now() in start..end 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<Semester>.getCurrentOrLast(): Semester { fun List<Semester>.getCurrentOrLast(): Semester {
if (isEmpty()) throw RuntimeException("Empty semester list") if (isEmpty()) throw RuntimeException("Empty semester list")
// when there is only one current semester // 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 // when there is more than one current semester - find one with higher id
singleOrNull { semester -> semester.semesterId == maxByOrNull { it.semesterId }?.semesterId }?.let { return it } singleOrNull { semester -> semester.semesterId == maxByOrNull { it.semesterId }?.semesterId }?.let { return it }

View File

@ -1,9 +1,5 @@
Wersja 2.0.0 Wersja 2.2.0
— zaktualizowaliśmy wygląd aplikacji na (częściowo) zgodny z wytycznymi Material 3 Dokonaliśmy drobnych usprawnień w obszarze logowania, które powinny pomóc Wam i nam
— dodaliśmy możliwość zmiany kolejności pozycji w menu dolnym
— poprawiliśmy sposób wyświetlania błędu o nieprawidłowym haśle na ekranie logowania
— od teraz zmiana ustawień liczenia średniej automatycznie odświeży listę ocen
— dodaliśmy okienko na wpisanie numeru PESEL, gdy dziennik wymaga dodatkowej autoryzacji dostępu
Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases

View File

@ -2,5 +2,5 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android" <shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle"> android:shape="rectangle">
<solid android:color="#FF000000" /> <solid android:color="#FF000000" />
<corners android:radius="12dp" /> <corners android:radius="14dp" />
</shape> </shape>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"> <shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#FFFFFBFF" /> <solid android:color="#FFFFFBFF" />
<corners android:radius="12dp" /> <corners android:radius="14dp" />
</shape> </shape>

View File

@ -0,0 +1,5 @@
<vector android:autoMirrored="true" android:height="24dp"
android:tint="#000000" android:viewportHeight="24"
android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M11,7L9.6,8.4l2.6,2.6H2v2h10.2l-2.6,2.6L11,17l5,-5L11,7zM20,19h-8v2h8c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2h-8v2h8V19z"/>
</vector>

View File

@ -1,6 +1,6 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:width="20dp"
android:height="24dp" android:height="20dp"
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24"> android:viewportHeight="24">
<path <path

View File

@ -1,6 +1,6 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:width="20dp"
android:height="24dp" android:height="20dp"
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24"> android:viewportHeight="24">
<path <path

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 75 KiB

View File

@ -5,10 +5,10 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="@drawable/background_widget_timetable" android:background="@drawable/background_widget_timetable"
android:backgroundTint="?attr/colorSurface" android:backgroundTint="?attr/colorSecondaryContainer"
android:clipToOutline="true" android:clipToOutline="true"
android:orientation="vertical" android:orientation="vertical"
android:paddingHorizontal="16dp" android:paddingHorizontal="12dp"
android:theme="@style/Wulkanowy.Widget.Theme" android:theme="@style/Wulkanowy.Widget.Theme"
tools:context=".ui.modules.timetablewidget.TimetableWidgetProvider"> tools:context=".ui.modules.timetablewidget.TimetableWidgetProvider">
@ -16,23 +16,47 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center_vertical" android:gravity="center_vertical"
android:orientation="horizontal" android:paddingTop="12dp"
android:paddingVertical="16dp"> android:paddingBottom="8dp">
<FrameLayout
android:layout_width="48dp"
android:layout_height="48dp">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:importantForAccessibility="no"
android:src="@drawable/background_timetable_widget_avatar"
android:tint="?attr/colorPrimary"
app:tint="?attr/colorPrimary"
tools:ignore="UseAppTint" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="JK"
android:textAppearance="?attr/textAppearanceTitleLarge"
android:textColor="?attr/colorOnPrimary"
android:textSize="18sp"
tools:ignore="HardcodedText" />
</FrameLayout>
<TextView <TextView
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="8dp" android:layout_marginHorizontal="12dp"
android:layout_weight="1" android:layout_weight="1"
android:lines="1" android:lines="1"
android:text="Pon, 03.10" android:text="Pon, 19.05"
android:textAppearance="?attr/textAppearanceHeadline5" android:textSize="18sp"
tools:ignore="HardcodedText" /> tools:ignore="HardcodedText" />
<ImageButton <ImageButton
android:layout_width="48dp" android:layout_width="48dp"
android:layout_height="48dp" android:layout_height="48dp"
android:layout_marginStart="12dp"
android:background="?attr/selectableItemBackgroundBorderless" android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/all_prev" android:contentDescription="@string/all_prev"
android:rotation="180" android:rotation="180"
@ -51,46 +75,273 @@
app:tint="?attr/colorPrimary" app:tint="?attr/colorPrimary"
tools:ignore="UseAppTint" /> tools:ignore="UseAppTint" />
<TextView
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginStart="12dp"
android:background="@drawable/background_timetable_widget_avatar"
android:backgroundTint="?attr/colorPrimary"
android:contentDescription="@string/account_quick_manager"
android:gravity="center"
android:text="AW"
android:textAppearance="?attr/textAppearanceTitleLarge"
android:textColor="?attr/colorOnPrimary" />
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
android:showDividers="middle"> android:paddingBottom="12dp">
<ImageView <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="56dp" android:layout_height="wrap_content"
android:layout_marginBottom="4dp" android:layout_marginBottom="4dp"
android:importantForAccessibility="no" android:background="@drawable/background_widget_item_timetable"
android:src="@drawable/background_widget_item_timetable" android:backgroundTint="?attr/colorSurface"
app:tint="?attr/backgroundColor" /> android:gravity="center_vertical"
android:minHeight="48dp"
android:orientation="horizontal"
android:paddingHorizontal="12dp"
android:paddingVertical="8dp">
<ImageView <TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="1"
android:textSize="22sp"
tools:ignore="HardcodedText" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:gravity="center_vertical"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="08:00"
android:textAppearance="?attr/textAppearanceBodySmall"
tools:ignore="HardcodedText" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:text="09:45"
android:textAppearance="?attr/textAppearanceBodySmall"
tools:ignore="HardcodedText" />
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:lines="1"
android:text="Wychowanie fizyczne"
android:textSize="14sp"
tools:ignore="HardcodedText" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="6dp"
android:text="213"
android:textSize="12sp"
tools:ignore="HardcodedText" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:lines="1"
android:text="Dorota Nowak"
android:textAppearance="?attr/textAppearanceBodySmall"
tools:ignore="HardcodedText" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="56dp" android:layout_height="wrap_content"
android:layout_marginBottom="4dp" android:layout_marginBottom="4dp"
android:importantForAccessibility="no" android:background="@drawable/background_widget_item_timetable"
android:src="@drawable/background_widget_item_timetable" android:backgroundTint="?attr/colorSurface"
app:tint="?attr/backgroundColor" /> android:gravity="center_vertical"
android:minHeight="48dp"
android:orientation="horizontal"
android:paddingHorizontal="12dp"
android:paddingVertical="8dp">
<ImageView <TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="2"
android:textSize="22sp"
tools:ignore="HardcodedText"
tools:textColor="?attr/colorTimetableChange" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:gravity="center_vertical"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="08:50"
android:textAppearance="?attr/textAppearanceBodySmall"
tools:ignore="HardcodedText" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:text="09:35"
android:textAppearance="?attr/textAppearanceBodySmall"
tools:ignore="HardcodedText" />
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:lines="1"
android:text="Język polski"
android:textColor="?attr/colorTimetableChange"
android:textSize="14sp"
tools:ignore="HardcodedText" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="6dp"
android:text="125"
android:textSize="12sp"
tools:ignore="HardcodedText" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:lines="1"
android:text="Karolina Kowalska"
android:textAppearance="?attr/textAppearanceBodySmall"
android:textColor="?attr/colorTimetableChange"
tools:ignore="HardcodedText" />
</LinearLayout>
</LinearLayout>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:contentDescription="@string/timetable_changes"
android:tint="?attr/colorTimetableChange"
app:tint="?attr/colorTimetableChange"
tools:ignore="UseAppTint"
tools:src="@drawable/ic_timetable_widget_swap" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="56dp" android:layout_height="wrap_content"
android:importantForAccessibility="no" android:background="@drawable/background_widget_item_timetable"
android:src="@drawable/background_widget_item_timetable" android:backgroundTint="?attr/colorSurface"
app:tint="?attr/backgroundColor" /> android:gravity="center_vertical"
android:minHeight="48dp"
android:orientation="horizontal"
android:paddingHorizontal="12dp"
android:paddingVertical="8dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="3"
android:textSize="22sp"
tools:ignore="HardcodedText"
tools:textColor="?attr/colorTimetableCanceled" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:gravity="center_vertical"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="09:45"
android:textAppearance="?attr/textAppearanceBodySmall"
tools:ignore="HardcodedText" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:text="10:30"
android:textAppearance="?attr/textAppearanceBodySmall"
tools:ignore="HardcodedText" />
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:lines="1"
android:text="Wiedza o społeczeństwie"
android:textColor="?attr/colorTimetableCanceled"
android:textSize="14sp"
tools:ignore="HardcodedText" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Lekcja odwołana: uczniowie zwolnieni do domu"
android:textAppearance="?attr/textAppearanceBodySmall"
android:textColor="?attr/colorTimetableCanceled"
tools:ignore="HardcodedText" />
</LinearLayout>
</LinearLayout>
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
@ -8,13 +9,18 @@
<com.google.android.material.textfield.TextInputLayout <com.google.android.material.textfield.TextInputLayout
style="@style/Widget.Material3.TextInputLayout.OutlinedBox" style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content"
app:counterEnabled="true"
app:counterMaxLength="256">
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
android:id="@+id/excuseReason" android:id="@+id/excuseReason"
style="@style/Widget.Material3.TextInputEditText.OutlinedBox"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:hint="@string/attendance_excuse_dialog_reason" /> android:layout_weight="1"
android:hint="@string/attendance_excuse_dialog_reason"
android:maxLength="256" />
<requestFocus /> <requestFocus />
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>

View File

@ -0,0 +1,92 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?android:windowBackground"
android:orientation="vertical">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/dialog_login_support_toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:navigationIcon="?attr/homeAsUpIndicator"
app:navigationIconTint="?attr/colorOnSurfaceVariant"
app:title="@string/login_support_title" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<LinearLayout
android:id="@+id/dialog_login_support_school_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="20dp"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/login_support_school_hint" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/dialog_login_support_school_layout"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:placeholderText="@string/login_support_school_placeholder"
app:placeholderTextColor="?colorTertiary">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/dialog_login_support_school_input"
style="@style/Widget.Material3.TextInputEditText.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="top" />
<requestFocus />
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/login_support_additional_hint" />
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="top"
app:placeholderText="@string/login_support_additional_placeholder"
app:placeholderTextColor="?colorTertiary">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/dialog_login_support_additional_input"
style="@style/Widget.Material3.TextInputEditText.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="top"
android:minLines="3" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/dialog_login_support_submit"
style="@style/Widget.Material3.Button.TextButton.Dialog"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginTop="12dp"
android:insetLeft="0dp"
android:insetTop="0dp"
android:insetRight="0dp"
android:insetBottom="0dp"
android:text="@string/login_support_submit" />
</LinearLayout>
</ScrollView>
</LinearLayout>

View File

@ -26,7 +26,8 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:clipToPadding="false" android:clipToPadding="false"
android:paddingTop="6dp" android:paddingTop="6dp"
android:paddingBottom="6dp" /> android:paddingBottom="6dp"
tools:listitem="@layout/item_dashboard_grades" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout> </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<LinearLayout <LinearLayout
@ -79,4 +80,4 @@
android:text="@string/all_retry" /> android:text="@string/all_retry" />
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -172,7 +172,7 @@
android:layout_marginBottom="16dp" android:layout_marginBottom="16dp"
android:hint="@string/login_host_hint" android:hint="@string/login_host_hint"
android:orientation="vertical" android:orientation="vertical"
app:layout_constraintBottom_toTopOf="@+id/loginFormTokenLayout" app:layout_constraintBottom_toTopOf="@+id/loginFormDomainSuffixLayout"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/loginFormPassLayout"> app:layout_constraintTop_toBottomOf="@+id/loginFormPassLayout">
@ -185,6 +185,31 @@
tools:ignore="Deprecated,LabelFor" /> tools:ignore="Deprecated,LabelFor" />
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/loginFormDomainSuffixLayout"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginLeft="24dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="24dp"
android:layout_marginRight="24dp"
android:hint="@string/login_domain_suffix_hint"
app:errorEnabled="true"
app:layout_constraintBottom_toTopOf="@+id/loginFormSymbolLayout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/loginFormHostLayout">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/loginFormDomainSuffix"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textShortMessage"
android:maxLines="1" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout <com.google.android.material.textfield.TextInputLayout
android:id="@+id/loginFormTokenLayout" android:id="@+id/loginFormTokenLayout"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox" style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
@ -200,7 +225,7 @@
app:layout_constraintBottom_toTopOf="@+id/loginFormSymbolLayout" app:layout_constraintBottom_toTopOf="@+id/loginFormSymbolLayout"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/loginFormHostLayout"> app:layout_constraintTop_toBottomOf="@+id/loginFormDomainSuffixLayout">
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
android:id="@+id/loginFormToken" android:id="@+id/loginFormToken"

View File

@ -105,6 +105,18 @@
android:background="?android:attr/listDivider" /> android:background="?android:attr/listDivider" />
</LinearLayout> </LinearLayout>
<include
android:id="@+id/login_form_message"
layout="@layout/item_dashboard_admin_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="16dp"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/loginFormContact"
app:layout_constraintVertical_chainStyle="packed"
tools:visibility="visible" />
<TextView <TextView
android:id="@+id/loginFormHeader" android:id="@+id/loginFormHeader"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -119,9 +131,8 @@
app:layout_constraintBottom_toTopOf="@+id/loginFormUsernameLayout" app:layout_constraintBottom_toTopOf="@+id/loginFormUsernameLayout"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/loginFormContact" app:layout_constraintTop_toBottomOf="@+id/login_form_message"
app:layout_constraintVertical_bias="0" app:layout_constraintVertical_bias="0"
app:layout_constraintVertical_chainStyle="packed"
app:layout_goneMarginTop="64dp" /> app:layout_goneMarginTop="64dp" />
<TextView <TextView
@ -239,6 +250,32 @@
tools:ignore="Deprecated,LabelFor" /> tools:ignore="Deprecated,LabelFor" />
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/loginFormDomainSuffixLayout"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginLeft="24dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="24dp"
android:layout_marginRight="24dp"
android:hint="@string/login_domain_suffix_hint"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/loginFormHostLayout"
tools:ignore="HardcodedText">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/loginFormDomainSuffix"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:importantForAutofill="no"
android:inputType="text"
android:maxLines="1"
tools:targetApi="o" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/loginFormAdvancedButton" android:id="@+id/loginFormAdvancedButton"
style="@style/Widget.Material3.Button.OutlinedButton" style="@style/Widget.Material3.Button.OutlinedButton"
@ -264,7 +301,7 @@
android:layout_marginEnd="24dp" android:layout_marginEnd="24dp"
android:text="@string/login_sign_in" android:text="@string/login_sign_in"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/loginFormHostLayout" /> app:layout_constraintTop_toBottomOf="@+id/loginFormDomainSuffixLayout" />
<TextView <TextView
android:id="@+id/loginFormPrivacyLink" android:id="@+id/loginFormPrivacyLink"

View File

@ -56,6 +56,8 @@
android:layout_marginBottom="32dp" android:layout_marginBottom="32dp"
android:enabled="false" android:enabled="false"
android:text="@string/login_sign_in" android:text="@string/login_sign_in"
app:icon="@drawable/ic_login"
app:iconGravity="end"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/loginStudentSelectRecycler" /> app:layout_constraintTop_toBottomOf="@id/loginStudentSelectRecycler" />

View File

@ -141,7 +141,9 @@
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5" app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/loginSymbolHelper"> app:layout_constraintTop_toBottomOf="@+id/loginSymbolHelper"
app:placeholderText="@string/login_symbol_placeholder"
app:placeholderTextColor="?colorTertiary">
<AutoCompleteTextView <AutoCompleteTextView
android:id="@+id/loginSymbolName" android:id="@+id/loginSymbolName"

View File

@ -14,16 +14,13 @@
<TextView <TextView
android:id="@+id/homework_dialog_header" android:id="@+id/homework_dialog_header"
android:layout_width="match_parent" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="24dp" android:layout_marginTop="24dp"
android:layout_weight="1"
android:text="@string/all_details" android:text="@string/all_details"
android:textAppearance="?attr/textAppearanceHeadlineSmall" android:textAppearance="?attr/textAppearanceHeadlineSmall"
android:textColor="?attr/colorOnSurface" android:textColor="?attr/colorOnSurface" />
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageButton <ImageButton
android:id="@+id/homework_dialog_delete" android:id="@+id/homework_dialog_delete"

View File

@ -0,0 +1,43 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?selectableItemBackground"
android:paddingStart="8dp"
android:paddingTop="6dp"
android:paddingEnd="12dp"
android:paddingBottom="6dp"
tools:context=".ui.modules.timetable.TimetableAdapter">
<TextView
android:id="@+id/timetableEmptyItemNumber"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:includeFontPadding="false"
android:maxLength="5"
android:minWidth="40dp"
android:minHeight="40dp"
android:textColor="?android:textColorHint"
android:textSize="32sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="1-4" />
<TextView
android:id="@+id/timetableEmptyItemSubject"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?android:textColorHint"
android:textSize="15sp"
app:layout_constraintBottom_toBottomOf="@+id/timetableEmptyItemNumber"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/timetableEmptyItemNumber"
app:layout_constraintTop_toTopOf="@+id/timetableEmptyItemNumber"
tools:text="No lessons" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -6,11 +6,12 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@drawable/background_widget_item_timetable" android:background="@drawable/background_widget_item_timetable"
android:backgroundTint="?attr/backgroundColor" android:backgroundTint="?attr/colorSurface"
android:gravity="center_vertical" android:gravity="center_vertical"
android:minHeight="48dp"
android:orientation="horizontal" android:orientation="horizontal"
android:paddingHorizontal="16dp" android:paddingHorizontal="12dp"
android:paddingVertical="12dp" android:paddingVertical="8dp"
android:theme="@style/Wulkanowy.Widget.Theme" android:theme="@style/Wulkanowy.Widget.Theme"
tools:context=".ui.modules.timetablewidget.TimetableWidgetFactory"> tools:context=".ui.modules.timetablewidget.TimetableWidgetFactory">
@ -18,15 +19,14 @@
android:id="@+id/timetableWidgetItemNumber" android:id="@+id/timetableWidgetItemNumber"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceHeadline6" android:textSize="22sp"
android:textSize="24sp"
tools:text="1" tools:text="1"
tools:textColor="?attr/colorTimetableChange" /> tools:textColor="?attr/colorTimetableChange" />
<LinearLayout <LinearLayout
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="12dp" android:layout_marginStart="10dp"
android:gravity="center_vertical" android:gravity="center_vertical"
android:orientation="vertical"> android:orientation="vertical">
@ -41,7 +41,7 @@
android:id="@+id/timetableWidgetItemTimeFinish" android:id="@+id/timetableWidgetItemTimeFinish"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="4dp" android:layout_marginTop="2dp"
android:textAppearance="?attr/textAppearanceBodySmall" android:textAppearance="?attr/textAppearanceBodySmall"
tools:text="09:45" /> tools:text="09:45" />
@ -60,7 +60,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:ellipsize="end" android:ellipsize="end"
android:lines="1" android:lines="1"
android:textAppearance="?attr/textAppearanceTitleMedium" android:textSize="14sp"
tools:text="Programowanie aplikacji mobilnych i desktopowych" /> tools:text="Programowanie aplikacji mobilnych i desktopowych" />
<LinearLayout <LinearLayout
@ -72,15 +72,15 @@
android:id="@+id/timetableWidgetItemRoom" android:id="@+id/timetableWidgetItemRoom"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="6dp"
android:textAppearance="?attr/textAppearanceBodySmall" android:textAppearance="?attr/textAppearanceBodySmall"
tools:text="Sala 213" tools:text="213"
tools:textColor="?attr/colorTimetableChange" /> tools:textColor="?attr/colorTimetableChange" />
<TextView <TextView
android:id="@+id/timetableWidgetItemTeacher" android:id="@+id/timetableWidgetItemTeacher"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:ellipsize="end" android:ellipsize="end"
android:lines="1" android:lines="1"
android:textAppearance="?attr/textAppearanceBodySmall" android:textAppearance="?attr/textAppearanceBodySmall"

View File

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/timetableWidgetEmptyItemContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/background_widget_item_timetable"
android:backgroundTint="?attr/colorSurface"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingHorizontal="12dp"
android:paddingVertical="8dp"
android:theme="@style/Wulkanowy.Widget.Theme"
tools:context=".ui.modules.timetablewidget.TimetableWidgetFactory">
<TextView
android:id="@+id/timetableWidgetEmptyItemNumber"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceHeadline6"
android:textColor="?android:textColorHint"
android:textSize="22sp"
tools:text="1-4" />
<TextView
android:id="@+id/timetableWidgetEmptyItemText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:ellipsize="end"
android:lines="1"
android:textAppearance="?attr/textAppearanceTitleMedium"
android:textColor="?android:textColorHint"
tools:text="No lessons" />
</LinearLayout>

View File

@ -5,35 +5,70 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="@drawable/background_widget_timetable" android:background="@drawable/background_widget_timetable"
android:backgroundTint="?attr/colorSurface" android:backgroundTint="?attr/colorSecondaryContainer"
android:clipToOutline="true" android:clipToOutline="true"
android:orientation="vertical" android:orientation="vertical"
android:paddingHorizontal="16dp" android:paddingHorizontal="12dp"
android:theme="@style/Wulkanowy.Widget.Theme" android:theme="@style/Wulkanowy.Widget.Theme"
tools:context=".ui.modules.timetablewidget.TimetableWidgetProvider"> tools:context=".ui.modules.timetablewidget.TimetableWidgetProvider"
tools:targetApi="s">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center_vertical" android:gravity="center_vertical"
android:orientation="horizontal" android:paddingTop="12dp"
android:paddingVertical="16dp"> android:paddingBottom="8dp">
<FrameLayout
android:id="@+id/timetableWidgetAccount"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="@drawable/background_timetable_widget_avatar"
android:backgroundTint="@android:color/transparent"
android:clickable="true"
android:clipToOutline="true"
android:contentDescription="@string/account_quick_manager"
android:focusable="true"
android:foreground="?attr/selectableItemBackgroundBorderless"
android:importantForAccessibility="yes"
android:outlineProvider="background"
tools:targetApi="s">
<ImageView
android:id="@+id/timetableWidgetAccountBackground"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:importantForAccessibility="no"
tools:src="@drawable/background_timetable_widget_avatar"
tools:tint="?attr/colorPrimary" />
<TextView
android:id="@+id/timetableWidgetAccountInitials"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:textAppearance="?attr/textAppearanceTitleLarge"
android:textColor="@android:color/white"
android:textSize="18sp"
tools:text="JK" />
</FrameLayout>
<TextView <TextView
android:id="@+id/timetableWidgetDate" android:id="@+id/timetableWidgetDate"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="8dp" android:layout_marginHorizontal="12dp"
android:layout_weight="1" android:layout_weight="1"
android:lines="1" android:lines="1"
android:textAppearance="?attr/textAppearanceHeadline5" android:textSize="18sp"
tools:text="Pon, 12.05" /> tools:text="Friday, 19.05" />
<ImageButton <ImageButton
android:id="@+id/timetableWidgetPrev" android:id="@+id/timetableWidgetPrev"
android:layout_width="48dp" android:layout_width="48dp"
android:layout_height="48dp" android:layout_height="48dp"
android:layout_marginStart="12dp"
android:background="?attr/selectableItemBackgroundBorderless" android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/all_prev" android:contentDescription="@string/all_prev"
android:rotation="180" android:rotation="180"
@ -53,39 +88,6 @@
app:tint="?attr/colorPrimary" app:tint="?attr/colorPrimary"
tools:ignore="UseAppTint" /> tools:ignore="UseAppTint" />
<FrameLayout
android:id="@+id/timetableWidgetAccount"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginStart="12dp"
android:background="@drawable/background_timetable_widget_avatar"
android:backgroundTint="@android:color/transparent"
android:clickable="true"
android:clipToOutline="true"
android:contentDescription="@string/account_quick_manager"
android:focusable="true"
android:foreground="?attr/selectableItemBackgroundBorderless"
android:importantForAccessibility="yes"
android:outlineProvider="background">
<ImageView
android:id="@+id/timetableWidgetAccountBackground"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:importantForAccessibility="no"
tools:src="@drawable/background_timetable_widget_avatar"
tools:tint="?attr/colorPrimary" />
<TextView
android:id="@+id/timetableWidgetAccountInitials"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:textAppearance="?attr/textAppearanceTitleLarge"
android:textColor="@android:color/white"
android:textSize="20sp"
tools:text="AW" />
</FrameLayout>
</LinearLayout> </LinearLayout>
<FrameLayout <FrameLayout
@ -99,6 +101,7 @@
android:clipToPadding="false" android:clipToPadding="false"
android:divider="@android:color/transparent" android:divider="@android:color/transparent"
android:dividerHeight="4dp" android:dividerHeight="4dp"
android:listSelector="@android:color/transparent"
android:paddingBottom="16dp" android:paddingBottom="16dp"
tools:listfooter="@layout/item_widget_timetable_footer" tools:listfooter="@layout/item_widget_timetable_footer"
tools:listitem="@layout/item_widget_timetable" /> tools:listitem="@layout/item_widget_timetable" />
@ -111,5 +114,7 @@
android:text="@string/widget_timetable_no_items" android:text="@string/widget_timetable_no_items"
android:textAppearance="?attr/textAppearanceBody1" android:textAppearance="?attr/textAppearanceBody1"
android:visibility="gone" /> android:visibility="gone" />
</FrameLayout> </FrameLayout>
</LinearLayout> </LinearLayout>

View File

@ -51,6 +51,11 @@
<item>Průměr z průměrů z obou semestrů</item> <item>Průměr z průměrů z obou semestrů</item>
<item>Průměr známek z celého roku</item> <item>Průměr známek z celého roku</item>
</string-array> </string-array>
<string-array name="timetable_show_gaps_entries">
<item>Nezobrazovat</item>
<item>Pouze mezi lekcemi</item>
<item>Před a mezi lekcemi</item>
</string-array>
<string-array name="dashboard_tile_entries"> <string-array name="dashboard_tile_entries">
<item>Šťastné číslo</item> <item>Šťastné číslo</item>
<item>Nepřečtené zprávy</item> <item>Nepřečtené zprávy</item>

View File

@ -37,12 +37,14 @@
<string name="login_login_pesel_email_hint">Přihlášení, číslo PESEL nebo e-mail</string> <string name="login_login_pesel_email_hint">Přihlášení, číslo PESEL nebo e-mail</string>
<string name="login_password_hint">Heslo</string> <string name="login_password_hint">Heslo</string>
<string name="login_host_hint">Variace deníku UONET+</string> <string name="login_host_hint">Variace deníku UONET+</string>
<string name="login_domain_suffix_hint">Vlastní přípona domény</string>
<string name="login_type_api">Mobile API</string> <string name="login_type_api">Mobile API</string>
<string name="login_type_scrapper">Scraper</string> <string name="login_type_scrapper">Scraper</string>
<string name="login_type_hybrid">Hybridní</string> <string name="login_type_hybrid">Hybridní</string>
<string name="login_token_hint">Token</string> <string name="login_token_hint">Token</string>
<string name="login_pin_hint">PIN</string> <string name="login_pin_hint">PIN</string>
<string name="login_symbol_hint">Symbol</string> <string name="login_symbol_hint">Symbol</string>
<string name="login_symbol_placeholder">E.g. \"lodz\" or \"powiatjaroslawski\"</string>
<string name="login_sign_in">Přihlásit</string> <string name="login_sign_in">Přihlásit</string>
<string name="login_invalid_password">Toto heslo je příliš krátké</string> <string name="login_invalid_password">Toto heslo je příliš krátké</string>
<string name="login_incorrect_password_default">Přihlašovací údaje jsou nesprávné</string> <string name="login_incorrect_password_default">Přihlašovací údaje jsou nesprávné</string>
@ -53,7 +55,8 @@
<string name="login_invalid_email">Neplatný e-mail</string> <string name="login_invalid_email">Neplatný e-mail</string>
<string name="login_invalid_login">Místo e-mailu použijte přiřazené přihlašovací údaje</string> <string name="login_invalid_login">Místo e-mailu použijte přiřazené přihlašovací údaje</string>
<string name="login_invalid_custom_email">Použijte přiřazené přihlašovací nebo e-mail v @%1$s</string> <string name="login_invalid_custom_email">Použijte přiřazené přihlašovací nebo e-mail v @%1$s</string>
<string name="login_invalid_symbol">Neplatný symbol</string> <string name="login_invalid_symbol">Invalid symbol. If you cannot find it, please contact the school</string>
<string name="login_invalid_symbol_definitely">Don\'t make this up! If you cannot find it, please contact the school</string>
<string name="login_incorrect_symbol">Žák nebyl nalezen. Zkontrolujte správnost symbolu a vybrané varianty deníku UONET+</string> <string name="login_incorrect_symbol">Žák nebyl nalezen. Zkontrolujte správnost symbolu a vybrané varianty deníku UONET+</string>
<string name="login_duplicate_student">Vybraný žák je už přihlášen</string> <string name="login_duplicate_student">Vybraný žák je už přihlášen</string>
<string name="login_symbol_helper">Symbol najdete na stránce deníku v &#160;<b>Uczeń</b>&#160;<b>Dostęp Mobilny</b>&#160;<b>Wygeneruj kod dostępu</b>.\n\nUjistěte se, že jste nastavili správnou variantu deníku v poli <b>Variace deníku UONET+</b> na první přihlašovací obrazovce</string> <string name="login_symbol_helper">Symbol najdete na stránce deníku v &#160;<b>Uczeń</b>&#160;<b>Dostęp Mobilny</b>&#160;<b>Wygeneruj kod dostępu</b>.\n\nUjistěte se, že jste nastavili správnou variantu deníku v poli <b>Variace deníku UONET+</b> na první přihlašovací obrazovce</string>
@ -68,7 +71,7 @@
<string name="login_contact_discord">Discord</string> <string name="login_contact_discord">Discord</string>
<string name="login_email_intent_title">Poslat e-mail</string> <string name="login_email_intent_title">Poslat e-mail</string>
<string name="login_recover_warning">Ujistěte se, že jste vybrali správnou variantu deníku UONET+!</string> <string name="login_recover_warning">Ujistěte se, že jste vybrali správnou variantu deníku UONET+!</string>
<string name="login_recover_button">Zapomněl jsem své heslo</string> <string name="login_recover_button">Reset password</string>
<string name="login_recover_title">Obnovte svůj účet</string> <string name="login_recover_title">Obnovte svůj účet</string>
<string name="login_recover">Obnovit</string> <string name="login_recover">Obnovit</string>
<string name="login_signed_in">Žák je už přihlášen</string> <string name="login_signed_in">Žák je už přihlášen</string>
@ -76,6 +79,12 @@
<string name="login_other_search_locations">Jiná místa vyhledávání</string> <string name="login_other_search_locations">Jiná místa vyhledávání</string>
<string name="login_no_active_student">Nebyli nalezeni žádní aktivní žáci</string> <string name="login_no_active_student">Nebyli nalezeni žádní aktivní žáci</string>
<string name="login_symbol_enter">Zadejte jiný symbol</string> <string name="login_symbol_enter">Zadejte jiný symbol</string>
<string name="login_support_title">Get help</string>
<string name="login_support_school_hint">Full school name (required)</string>
<string name="login_support_school_placeholder">Np. ZSTiO Jarosław lub SP nr 99 w Łodzi</string>
<string name="login_support_additional_hint">Additional information in Polish (optional)</string>
<string name="login_support_additional_placeholder">Np. \"Ostatnio zmieniłem szkołę i…\" albo \"Jestem rodzicem i nie widzę drugiego dziecka…\"</string>
<string name="login_support_submit">Submit</string>
<!--Notifications--> <!--Notifications-->
<string name="notifications_header_title">Povolit oznámení</string> <string name="notifications_header_title">Povolit oznámení</string>
<string name="notifications_header_description">Povolit upozornění, abyste nezmeškali zprávu od učitele nebo o nové známce</string> <string name="notifications_header_description">Povolit upozornění, abyste nezmeškali zprávu od učitele nebo o nové známce</string>
@ -184,6 +193,12 @@
<string name="timetable_notify_change_room">Změna učebny z %1$s na %2$s</string> <string name="timetable_notify_change_room">Změna učebny z %1$s na %2$s</string>
<string name="timetable_notify_change_teacher">Změna učitele z %1$s na %2$s</string> <string name="timetable_notify_change_teacher">Změna učitele z %1$s na %2$s</string>
<string name="timetable_notify_change_subject">Změna předmětu z %1$s na %2$s</string> <string name="timetable_notify_change_subject">Změna předmětu z %1$s na %2$s</string>
<plurals name="timetable_no_lesson">
<item quantity="one">Žádné lekce</item>
<item quantity="few">Žádné lekce</item>
<item quantity="many">Žádné lekce</item>
<item quantity="other">Žádné lekce</item>
</plurals>
<plurals name="timetable_notify_new_items_title"> <plurals name="timetable_notify_new_items_title">
<item quantity="one">Změna plánu lekcí</item> <item quantity="one">Změna plánu lekcí</item>
<item quantity="few">Změny plánu lekcí</item> <item quantity="few">Změny plánu lekcí</item>
@ -351,6 +366,8 @@
</plurals> </plurals>
<string name="message_messages_deleted">Zprávy odstraněné</string> <string name="message_messages_deleted">Zprávy odstraněné</string>
<string name="message_mailbox_chooser_title">Vyberte poštovní schránku</string> <string name="message_mailbox_chooser_title">Vyberte poštovní schránku</string>
<string name="message_incognito_mode_on">Anonymní režim je zapnutý</string>
<string name="message_incognito_description">Díky anonymnímu režimu není odesílatel upozorněn, když si zprávu přečtete</string>
<!--Note--> <!--Note-->
<string name="note_no_items">Žádné informace o poznámkách</string> <string name="note_no_items">Žádné informace o poznámkách</string>
<string name="note_points">Body</string> <string name="note_points">Body</string>
@ -697,6 +714,7 @@
<string name="pref_view_expand_grade">Rozvíjení známek</string> <string name="pref_view_expand_grade">Rozvíjení známek</string>
<string name="pref_view_timetable_show_timers">Označit aktuální lekci</string> <string name="pref_view_timetable_show_timers">Označit aktuální lekci</string>
<string name="pref_view_timetable_show_groups">Zobrazit skupiny vedle předmětů</string> <string name="pref_view_timetable_show_groups">Zobrazit skupiny vedle předmětů</string>
<string name="pref_view_timetable_show_gaps">Zobrazit prázdné dlaždice, kde není žádná lekce</string>
<string name="pref_view_grade_statistics_list">Zobrazit seznam grafů v známkách třídy</string> <string name="pref_view_grade_statistics_list">Zobrazit seznam grafů v známkách třídy</string>
<string name="pref_view_subjects_without_grades">Zobrazit předměty bez známek</string> <string name="pref_view_subjects_without_grades">Zobrazit předměty bez známek</string>
<string name="pref_view_grade_color_scheme">Známky barevné schéma</string> <string name="pref_view_grade_color_scheme">Známky barevné schéma</string>
@ -737,6 +755,8 @@
<string name="pref_other_grade_modifier_minus">Hodnota mínusu</string> <string name="pref_other_grade_modifier_minus">Hodnota mínusu</string>
<string name="pref_other_fill_message_content">Odpovědět s historií zpráv</string> <string name="pref_other_fill_message_content">Odpovědět s historií zpráv</string>
<string name="pref_other_optional_arithmetic_average">Vypočítat aritmetický průměr, pokud žádná známka nemá váhu</string> <string name="pref_other_optional_arithmetic_average">Vypočítat aritmetický průměr, pokud žádná známka nemá váhu</string>
<string name="pref_other_incognito_mode">Anonymní režim</string>
<string name="pref_other_incognito_mode_summary">Neinformovat o přečtení zprávy</string>
<string name="pref_ads_support_category_name">Podpora</string> <string name="pref_ads_support_category_name">Podpora</string>
<string name="pref_ads_privacy_policy">Ochrana osobních údajů</string> <string name="pref_ads_privacy_policy">Ochrana osobních údajů</string>
<string name="pref_ads_agreements">Souhlasy</string> <string name="pref_ads_agreements">Souhlasy</string>
@ -809,6 +829,15 @@
<string name="menu_order_confirm_title">Restartování aplikace</string> <string name="menu_order_confirm_title">Restartování aplikace</string>
<string name="menu_order_confirm_content">Pro uložení změn je nutné aplikaci restartovat</string> <string name="menu_order_confirm_content">Pro uložení změn je nutné aplikaci restartovat</string>
<string name="menu_order_confirm_restart">Restartovat</string> <string name="menu_order_confirm_restart">Restartovat</string>
<!--Auth-->
<string name="auth_api_error">Autorizace byla zamítnuta. Uvedené údaje se neshodují se záznamy v kanceláři tajemníka.</string>
<string name="auth_invalid_error">Neplatný PESEL</string>
<string name="auth_pesel">PESEL</string>
<string name="auth_button">Autorizovat</string>
<string name="auth_success">Autorizace byla úspěšně dokončena</string>
<string name="auth_title">Autorizace</string>
<string name="auth_description">Pro provoz aplikace potřebujeme potvrdit vaši identitu. Zadejte PESEL žáka &lt;b&gt;%1$s&lt;/b&gt; v níže uvedeném poli</string>
<string name="auth_button_skip">Zatím přeskočit</string>
<!--Errors--> <!--Errors-->
<string name="error_no_internet">Žádné internetové připojení</string> <string name="error_no_internet">Žádné internetové připojení</string>
<string name="error_invalid_device_datetime">Vyskytla se chyba. Zkontrolujte hodiny svého zařízení</string> <string name="error_invalid_device_datetime">Vyskytla se chyba. Zkontrolujte hodiny svého zařízení</string>

View File

@ -51,6 +51,11 @@
<item>Average of averages from both semesters</item> <item>Average of averages from both semesters</item>
<item>Average of grades from the whole year</item> <item>Average of grades from the whole year</item>
</string-array> </string-array>
<string-array name="timetable_show_gaps_entries">
<item>Don\'t show</item>
<item>Only between lessons</item>
<item>Before and between lessons</item>
</string-array>
<string-array name="dashboard_tile_entries"> <string-array name="dashboard_tile_entries">
<item>Lucky number</item> <item>Lucky number</item>
<item>Unread messages</item> <item>Unread messages</item>

View File

@ -37,12 +37,14 @@
<string name="login_login_pesel_email_hint">Login, PESEL or e-mail</string> <string name="login_login_pesel_email_hint">Login, PESEL or e-mail</string>
<string name="login_password_hint">Password</string> <string name="login_password_hint">Password</string>
<string name="login_host_hint">UONET+ register variant</string> <string name="login_host_hint">UONET+ register variant</string>
<string name="login_domain_suffix_hint">Custom domain suffix</string>
<string name="login_type_api">Mobile API</string> <string name="login_type_api">Mobile API</string>
<string name="login_type_scrapper">Scraper</string> <string name="login_type_scrapper">Scraper</string>
<string name="login_type_hybrid">Hybrid</string> <string name="login_type_hybrid">Hybrid</string>
<string name="login_token_hint">Token</string> <string name="login_token_hint">Token</string>
<string name="login_pin_hint">PIN</string> <string name="login_pin_hint">PIN</string>
<string name="login_symbol_hint">Symbol</string> <string name="login_symbol_hint">Symbol</string>
<string name="login_symbol_placeholder">E.g. \"lodz\" or \"powiatjaroslawski\"</string>
<string name="login_sign_in">Sign in</string> <string name="login_sign_in">Sign in</string>
<string name="login_invalid_password">Password too short</string> <string name="login_invalid_password">Password too short</string>
<string name="login_incorrect_password_default">Login details are incorrect</string> <string name="login_incorrect_password_default">Login details are incorrect</string>
@ -53,7 +55,8 @@
<string name="login_invalid_email">Invalid email</string> <string name="login_invalid_email">Invalid email</string>
<string name="login_invalid_login">Use the assigned login instead of email</string> <string name="login_invalid_login">Use the assigned login instead of email</string>
<string name="login_invalid_custom_email">Use the assigned login or email in @%1$s</string> <string name="login_invalid_custom_email">Use the assigned login or email in @%1$s</string>
<string name="login_invalid_symbol">Invalid symbol</string> <string name="login_invalid_symbol">Invalid symbol. If you cannot find it, please contact the school</string>
<string name="login_invalid_symbol_definitely">Don\'t make this up! If you cannot find it, please contact the school</string>
<string name="login_incorrect_symbol">Student not found. Validate the symbol and the chosen variation of the UONET+ register</string> <string name="login_incorrect_symbol">Student not found. Validate the symbol and the chosen variation of the UONET+ register</string>
<string name="login_duplicate_student">Selected student is already logged in</string> <string name="login_duplicate_student">Selected student is already logged in</string>
<string name="login_symbol_helper">The symbol can be found on the register page in&#160;<b>Uczeń</b> →&#160;<b>Dostęp Mobilny</b>&#160;<b>Wygeneruj kod dostępu</b>.\n\nMake sure that you have set the appropriate register variant in the <b>UONET+ register variant</b> field on the first login screen</string> <string name="login_symbol_helper">The symbol can be found on the register page in&#160;<b>Uczeń</b> →&#160;<b>Dostęp Mobilny</b>&#160;<b>Wygeneruj kod dostępu</b>.\n\nMake sure that you have set the appropriate register variant in the <b>UONET+ register variant</b> field on the first login screen</string>
@ -68,7 +71,7 @@
<string name="login_contact_discord">Discord</string> <string name="login_contact_discord">Discord</string>
<string name="login_email_intent_title">Send email</string> <string name="login_email_intent_title">Send email</string>
<string name="login_recover_warning">Make sure you select the correct UONET+ register variation!</string> <string name="login_recover_warning">Make sure you select the correct UONET+ register variation!</string>
<string name="login_recover_button">I forgot my password</string> <string name="login_recover_button">Reset password</string>
<string name="login_recover_title">Recover your account</string> <string name="login_recover_title">Recover your account</string>
<string name="login_recover">Recover</string> <string name="login_recover">Recover</string>
<string name="login_signed_in">Student is already signed in</string> <string name="login_signed_in">Student is already signed in</string>
@ -76,6 +79,12 @@
<string name="login_other_search_locations">Other search locations</string> <string name="login_other_search_locations">Other search locations</string>
<string name="login_no_active_student">No active students found</string> <string name="login_no_active_student">No active students found</string>
<string name="login_symbol_enter">Enter a different symbol</string> <string name="login_symbol_enter">Enter a different symbol</string>
<string name="login_support_title">Get help</string>
<string name="login_support_school_hint">Full school name (required)</string>
<string name="login_support_school_placeholder">Np. ZSTiO Jarosław lub SP nr 99 w Łodzi</string>
<string name="login_support_additional_hint">Additional information in Polish (optional)</string>
<string name="login_support_additional_placeholder">Np. \"Ostatnio zmieniłem szkołę i…\" albo \"Jestem rodzicem i nie widzę drugiego dziecka…\"</string>
<string name="login_support_submit">Submit</string>
<!--Notifications--> <!--Notifications-->
<string name="notifications_header_title">Enable notifications</string> <string name="notifications_header_title">Enable notifications</string>
<string name="notifications_header_description">Enable notifications so you don\'t miss message from teacher or new grade</string> <string name="notifications_header_description">Enable notifications so you don\'t miss message from teacher or new grade</string>
@ -170,6 +179,10 @@
<string name="timetable_notify_change_room">Change of room from %1$s to %2$s</string> <string name="timetable_notify_change_room">Change of room from %1$s to %2$s</string>
<string name="timetable_notify_change_teacher">Change of teacher from %1$s to %2$s</string> <string name="timetable_notify_change_teacher">Change of teacher from %1$s to %2$s</string>
<string name="timetable_notify_change_subject">Change of subject from %1$s to %2$s</string> <string name="timetable_notify_change_subject">Change of subject from %1$s to %2$s</string>
<plurals name="timetable_no_lesson">
<item quantity="one">No lesson</item>
<item quantity="other">No lessons</item>
</plurals>
<plurals name="timetable_notify_new_items_title"> <plurals name="timetable_notify_new_items_title">
<item quantity="one">Timetable change</item> <item quantity="one">Timetable change</item>
<item quantity="other">Timetable changes</item> <item quantity="other">Timetable changes</item>
@ -309,6 +322,8 @@
</plurals> </plurals>
<string name="message_messages_deleted">Messages deleted</string> <string name="message_messages_deleted">Messages deleted</string>
<string name="message_mailbox_chooser_title">Choose mailbox</string> <string name="message_mailbox_chooser_title">Choose mailbox</string>
<string name="message_incognito_mode_on">Incognito mode is on</string>
<string name="message_incognito_description">Thanks to incognito mode sender is not notified when you read the message</string>
<!--Note--> <!--Note-->
<string name="note_no_items">No info about notes</string> <string name="note_no_items">No info about notes</string>
<string name="note_points">Points</string> <string name="note_points">Points</string>
@ -609,6 +624,7 @@
<string name="pref_view_expand_grade">Grades expanding</string> <string name="pref_view_expand_grade">Grades expanding</string>
<string name="pref_view_timetable_show_timers">Mark current lesson</string> <string name="pref_view_timetable_show_timers">Mark current lesson</string>
<string name="pref_view_timetable_show_groups">Show groups next to subjects</string> <string name="pref_view_timetable_show_groups">Show groups next to subjects</string>
<string name="pref_view_timetable_show_gaps">Show empty tiles where there\'s no lesson</string>
<string name="pref_view_grade_statistics_list">Show chart list in class grades</string> <string name="pref_view_grade_statistics_list">Show chart list in class grades</string>
<string name="pref_view_subjects_without_grades">Show subjects without grades</string> <string name="pref_view_subjects_without_grades">Show subjects without grades</string>
<string name="pref_view_grade_color_scheme">Grades color scheme</string> <string name="pref_view_grade_color_scheme">Grades color scheme</string>
@ -649,6 +665,8 @@
<string name="pref_other_grade_modifier_minus">Value of the minus</string> <string name="pref_other_grade_modifier_minus">Value of the minus</string>
<string name="pref_other_fill_message_content">Reply with message history</string> <string name="pref_other_fill_message_content">Reply with message history</string>
<string name="pref_other_optional_arithmetic_average">Show arithmetic average when no weights provided</string> <string name="pref_other_optional_arithmetic_average">Show arithmetic average when no weights provided</string>
<string name="pref_other_incognito_mode">Incognito mode</string>
<string name="pref_other_incognito_mode_summary">Do not inform about reading the message</string>
<string name="pref_ads_support_category_name">Support</string> <string name="pref_ads_support_category_name">Support</string>
<string name="pref_ads_privacy_policy">Privacy Policy</string> <string name="pref_ads_privacy_policy">Privacy Policy</string>
<string name="pref_ads_agreements">Agreements</string> <string name="pref_ads_agreements">Agreements</string>
@ -721,6 +739,15 @@
<string name="menu_order_confirm_title">Application restart</string> <string name="menu_order_confirm_title">Application restart</string>
<string name="menu_order_confirm_content">The application must restart for the changes to be saved</string> <string name="menu_order_confirm_content">The application must restart for the changes to be saved</string>
<string name="menu_order_confirm_restart">Restart</string> <string name="menu_order_confirm_restart">Restart</string>
<!--Auth-->
<string name="auth_api_error">Authorization has been rejected. The data provided does not match the records in the secretary\'s office.</string>
<string name="auth_invalid_error">Invalid PESEL</string>
<string name="auth_pesel">PESEL</string>
<string name="auth_button">Authorize</string>
<string name="auth_success">Authorization completed successfully</string>
<string name="auth_title">Authorization</string>
<string name="auth_description">To operate the application, we need to confirm your identity. Please enter the student\'s PESEL &lt;b&gt;%1$s&lt;/b&gt; in the field below</string>
<string name="auth_button_skip">Skip for now</string>
<!--Errors--> <!--Errors-->
<string name="error_no_internet">No internet connection</string> <string name="error_no_internet">No internet connection</string>
<string name="error_invalid_device_datetime">An error occurred. Check your device clock</string> <string name="error_invalid_device_datetime">An error occurred. Check your device clock</string>

View File

@ -51,6 +51,11 @@
<item>Durchschnittswert der Durchschnittswerte beider Semester</item> <item>Durchschnittswert der Durchschnittswerte beider Semester</item>
<item>Durchschnitt der Noten aus dem ganzen Jahr</item> <item>Durchschnitt der Noten aus dem ganzen Jahr</item>
</string-array> </string-array>
<string-array name="timetable_show_gaps_entries">
<item>Don\'t show</item>
<item>Only between lessons</item>
<item>Before and between lessons</item>
</string-array>
<string-array name="dashboard_tile_entries"> <string-array name="dashboard_tile_entries">
<item>Glückszahl</item> <item>Glückszahl</item>
<item>Ungelesene Nachrichten</item> <item>Ungelesene Nachrichten</item>

View File

@ -26,7 +26,7 @@
<string name="student_info_title">Schülerinfo</string> <string name="student_info_title">Schülerinfo</string>
<string name="dashboard_title">Übersicht</string> <string name="dashboard_title">Übersicht</string>
<string name="notifications_center_title">Benachrichtigungszentrum</string> <string name="notifications_center_title">Benachrichtigungszentrum</string>
<string name="menu_order_title">Menu configuartion</string> <string name="menu_order_title">Menü Konfiguration</string>
<!--Subtitles--> <!--Subtitles-->
<string name="grade_subtitle">Semester %1$d, %2$d/%3$d</string> <string name="grade_subtitle">Semester %1$d, %2$d/%3$d</string>
<!--Login--> <!--Login-->
@ -37,12 +37,14 @@
<string name="login_login_pesel_email_hint">Anmeldung, PESEL oder e-mail</string> <string name="login_login_pesel_email_hint">Anmeldung, PESEL oder e-mail</string>
<string name="login_password_hint">Passwort</string> <string name="login_password_hint">Passwort</string>
<string name="login_host_hint">UONET+ Registervariante</string> <string name="login_host_hint">UONET+ Registervariante</string>
<string name="login_domain_suffix_hint">Custom domain suffix</string>
<string name="login_type_api">Mobile API</string> <string name="login_type_api">Mobile API</string>
<string name="login_type_scrapper">Scraper</string> <string name="login_type_scrapper">Scraper</string>
<string name="login_type_hybrid">Hybride</string> <string name="login_type_hybrid">Hybride</string>
<string name="login_token_hint">Token</string> <string name="login_token_hint">Token</string>
<string name="login_pin_hint">PIN</string> <string name="login_pin_hint">PIN</string>
<string name="login_symbol_hint">Symbol</string> <string name="login_symbol_hint">Symbol</string>
<string name="login_symbol_placeholder">E.g. \"lodz\" or \"powiatjaroslawski\"</string>
<string name="login_sign_in">Anmelden</string> <string name="login_sign_in">Anmelden</string>
<string name="login_invalid_password">Passwort ist zu kurz</string> <string name="login_invalid_password">Passwort ist zu kurz</string>
<string name="login_incorrect_password_default">Anmeldedaten sind falsch</string> <string name="login_incorrect_password_default">Anmeldedaten sind falsch</string>
@ -53,10 +55,11 @@
<string name="login_invalid_email">Ungültige email</string> <string name="login_invalid_email">Ungültige email</string>
<string name="login_invalid_login">Den zugewiesenen Login anstelle von email verwenden</string> <string name="login_invalid_login">Den zugewiesenen Login anstelle von email verwenden</string>
<string name="login_invalid_custom_email">Benutze den zugewiesenen Login oder E-Mail in @%1$s</string> <string name="login_invalid_custom_email">Benutze den zugewiesenen Login oder E-Mail in @%1$s</string>
<string name="login_invalid_symbol">Ungültige symbol</string> <string name="login_invalid_symbol">Invalid symbol. If you cannot find it, please contact the school</string>
<string name="login_invalid_symbol_definitely">Don\'t make this up! If you cannot find it, please contact the school</string>
<string name="login_incorrect_symbol">Schüler nicht gefunden. Überprüfen Sie das Symbol und die gewählte Variation des UONET+ Registers</string> <string name="login_incorrect_symbol">Schüler nicht gefunden. Überprüfen Sie das Symbol und die gewählte Variation des UONET+ Registers</string>
<string name="login_duplicate_student">Ausgewählter Student ist bereits angemeldet.</string> <string name="login_duplicate_student">Ausgewählter Student ist bereits angemeldet.</string>
<string name="login_symbol_helper">The symbol can be found on the register page in&#160;<b>Uczeń</b> &#160;<b>Dostęp Mobilny</b>&#160;<b>Wygeneruj kod dostępu</b>.\n\nMake sure that you have set the appropriate register variant in the <b>UONET+ register variant</b> field on the first login screen</string> <string name="login_symbol_helper">Das Symbol kann auf der Registerseite in&#160;<b>Student </b>&#160;<b>Tost Möbeln</b>&#160;<b>Registrieren Sie Ihr Mobilgerät</b>gefunden werden.\n\nStellen Sie sicher, dass Sie die entsprechende Registervariante im Feld <b>UONET+ Registervariante</b> auf dem vorherigen Bildschirm festgelegt haben</string>
<string name="login_select_student">Wählen Sie die Studenten aus, die sich bei der Anwendung anmelden sollen</string> <string name="login_select_student">Wählen Sie die Studenten aus, die sich bei der Anwendung anmelden sollen</string>
<string name="login_advanced">Andere Optionen</string> <string name="login_advanced">Andere Optionen</string>
<string name="login_advanced_warning_mobile_api">In diesem Modus funktioniert eine Glücknummer, eine Klassenstatistik, eine Zusammenfassung der Anwesenheit, eine Entschuldigung für die Abwesenheit, abgeschlossene Lektionen, Schulinformationen und eine Vorschau der Liste der registrierten Geräte nicht</string> <string name="login_advanced_warning_mobile_api">In diesem Modus funktioniert eine Glücknummer, eine Klassenstatistik, eine Zusammenfassung der Anwesenheit, eine Entschuldigung für die Abwesenheit, abgeschlossene Lektionen, Schulinformationen und eine Vorschau der Liste der registrierten Geräte nicht</string>
@ -68,19 +71,25 @@
<string name="login_contact_discord">Discord</string> <string name="login_contact_discord">Discord</string>
<string name="login_email_intent_title">email senden</string> <string name="login_email_intent_title">email senden</string>
<string name="login_recover_warning">Stellen Sie sicher, dass Sie die richtige UONET+ Registervariation wählen!</string> <string name="login_recover_warning">Stellen Sie sicher, dass Sie die richtige UONET+ Registervariation wählen!</string>
<string name="login_recover_button">Ich habe mein Passwort vergessen.</string> <string name="login_recover_button">Reset password</string>
<string name="login_recover_title">Ihr Konto wiederherstellen</string> <string name="login_recover_title">Ihr Konto wiederherstellen</string>
<string name="login_recover">Wiederherstellen</string> <string name="login_recover">Wiederherstellen</string>
<string name="login_signed_in">Student ist bereits angemeldet</string> <string name="login_signed_in">Student ist bereits angemeldet</string>
<string name="login_host_standard">Standard</string> <string name="login_host_standard">Standard</string>
<string name="login_other_search_locations">Other search locations</string> <string name="login_other_search_locations">Andere Suchorte</string>
<string name="login_no_active_student">No active students found</string> <string name="login_no_active_student">Keine aktiven Schüler gefunden</string>
<string name="login_symbol_enter">Enter a different symbol</string> <string name="login_symbol_enter">Geben Sie ein anderes Symbol ein</string>
<string name="login_support_title">Get help</string>
<string name="login_support_school_hint">Full school name (required)</string>
<string name="login_support_school_placeholder">Np. ZSTiO Jarosław lub SP nr 99 w Łodzi</string>
<string name="login_support_additional_hint">Additional information in Polish (optional)</string>
<string name="login_support_additional_placeholder">Np. \"Ostatnio zmieniłem szkołę i…\" albo \"Jestem rodzicem i nie widzę drugiego dziecka…\"</string>
<string name="login_support_submit">Submit</string>
<!--Notifications--> <!--Notifications-->
<string name="notifications_header_title">Enable notifications</string> <string name="notifications_header_title">Benachrichtigungen aktivieren</string>
<string name="notifications_header_description">Enable notifications so you don\'t miss message from teacher or new grade</string> <string name="notifications_header_description">Aktivieren Sie Benachrichtigungen, damit Sie keine Nachricht vom Lehrer oder eine neue Klasse verpassen</string>
<string name="notifications_skip">Skip</string> <string name="notifications_skip">Überspringen</string>
<string name="notifications_enable">Enable</string> <string name="notifications_enable">Ermöglichen</string>
<!--Main--> <!--Main-->
<string name="main_account_picker">Kundenbetreuer</string> <string name="main_account_picker">Kundenbetreuer</string>
<string name="main_log_in">Anmelden</string> <string name="main_log_in">Anmelden</string>
@ -170,6 +179,10 @@
<string name="timetable_notify_change_room">Änderung des Raumes von %1$s zu %2$s</string> <string name="timetable_notify_change_room">Änderung des Raumes von %1$s zu %2$s</string>
<string name="timetable_notify_change_teacher">Wechsel des Lehrers von %1$s zu %2$s</string> <string name="timetable_notify_change_teacher">Wechsel des Lehrers von %1$s zu %2$s</string>
<string name="timetable_notify_change_subject">Thema von %1$s zu %2$s wechseln</string> <string name="timetable_notify_change_subject">Thema von %1$s zu %2$s wechseln</string>
<plurals name="timetable_no_lesson">
<item quantity="one">No lesson</item>
<item quantity="other">No lessons</item>
</plurals>
<plurals name="timetable_notify_new_items_title"> <plurals name="timetable_notify_new_items_title">
<item quantity="one">Änderung des Zeitplans</item> <item quantity="one">Änderung des Zeitplans</item>
<item quantity="other">Änderungen des Zeitplans</item> <item quantity="other">Änderungen des Zeitplans</item>
@ -288,7 +301,7 @@
<string name="message_chip_only_unread">Nur ungelesen</string> <string name="message_chip_only_unread">Nur ungelesen</string>
<string name="message_chip_only_with_attachments">Nur mit Anhängen</string> <string name="message_chip_only_with_attachments">Nur mit Anhängen</string>
<string name="message_read">Lesen: %s</string> <string name="message_read">Lesen: %s</string>
<string name="message_read_by">Read by: %1$d of %2$d people</string> <string name="message_read_by">Lesen von: %1$d von %2$d Personen</string>
<plurals name="message_number_item"> <plurals name="message_number_item">
<item quantity="one">%1$d Nachricht</item> <item quantity="one">%1$d Nachricht</item>
<item quantity="other">%1$d Nachrichten</item> <item quantity="other">%1$d Nachrichten</item>
@ -309,6 +322,8 @@
</plurals> </plurals>
<string name="message_messages_deleted">Nachrichten gelöscht</string> <string name="message_messages_deleted">Nachrichten gelöscht</string>
<string name="message_mailbox_chooser_title">Postfach auswählen</string> <string name="message_mailbox_chooser_title">Postfach auswählen</string>
<string name="message_incognito_mode_on">Incognito mode is on</string>
<string name="message_incognito_description">Thanks to incognito mode sender is not notified when you read the message</string>
<!--Note--> <!--Note-->
<string name="note_no_items">Keine Informationen über Eintragen</string> <string name="note_no_items">Keine Informationen über Eintragen</string>
<string name="note_points">Punkte</string> <string name="note_points">Punkte</string>
@ -422,8 +437,8 @@
</plurals> </plurals>
<string name="conferences_present">Teilnahme an einem Meeting</string> <string name="conferences_present">Teilnahme an einem Meeting</string>
<string name="conference_agenda">Agenda</string> <string name="conference_agenda">Agenda</string>
<string name="conference_place">Place</string> <string name="conference_place">Ort</string>
<string name="conference_topic">Topic</string> <string name="conference_topic">Thema</string>
<!--Director information--> <!--Director information-->
<string name="school_announcement_title">Schulankündigungen</string> <string name="school_announcement_title">Schulankündigungen</string>
<string name="school_announcement_no_items">Keine schulankündigungen</string> <string name="school_announcement_no_items">Keine schulankündigungen</string>
@ -591,10 +606,10 @@
<string name="all_undo">lösen</string> <string name="all_undo">lösen</string>
<string name="all_change">Ändern</string> <string name="all_change">Ändern</string>
<string name="all_add_to_calendar">Zum Kalender hinzufügen</string> <string name="all_add_to_calendar">Zum Kalender hinzufügen</string>
<string name="all_cancel">Cancel</string> <string name="all_cancel">Stornieren</string>
<!--Timetable Widget--> <!--Timetable Widget-->
<string name="widget_timetable_no_items">Keine Lektionen</string> <string name="widget_timetable_no_items">Keine Lektionen</string>
<string name="widget_timetable_last_synchronization">Synchronized on %1$s at %2$s</string> <string name="widget_timetable_last_synchronization">Synchronisiert am %1$s am %2$s</string>
<string name="widget_timetable_theme_title">Thema wählen</string> <string name="widget_timetable_theme_title">Thema wählen</string>
<string name="widget_timetable_theme_light">Licht</string> <string name="widget_timetable_theme_light">Licht</string>
<string name="widget_timetable_theme_dark">Dunkel</string> <string name="widget_timetable_theme_dark">Dunkel</string>
@ -609,13 +624,14 @@
<string name="pref_view_expand_grade">Steigende Sorten</string> <string name="pref_view_expand_grade">Steigende Sorten</string>
<string name="pref_view_timetable_show_timers">Aktuelle Lektion markieren</string> <string name="pref_view_timetable_show_timers">Aktuelle Lektion markieren</string>
<string name="pref_view_timetable_show_groups">Gruppen neben Schulfächen anzeigen</string> <string name="pref_view_timetable_show_groups">Gruppen neben Schulfächen anzeigen</string>
<string name="pref_view_timetable_show_gaps">Show empty tiles where there\'s no lesson</string>
<string name="pref_view_grade_statistics_list">Liste der Diagramme in Klassenbewertungen anzeigen</string> <string name="pref_view_grade_statistics_list">Liste der Diagramme in Klassenbewertungen anzeigen</string>
<string name="pref_view_subjects_without_grades">Schulfächer ohne Noten anzeigen</string> <string name="pref_view_subjects_without_grades">Schulfächer ohne Noten anzeigen</string>
<string name="pref_view_grade_color_scheme">Farbschema der Noten</string> <string name="pref_view_grade_color_scheme">Farbschema der Noten</string>
<string name="pref_view_grade_sorting_mode">Schulfachen sortieren</string> <string name="pref_view_grade_sorting_mode">Schulfachen sortieren</string>
<string name="pref_view_app_language">Sprache</string> <string name="pref_view_app_language">Sprache</string>
<string name="pref_view_menu_order_title">Menu configuration</string> <string name="pref_view_menu_order_title">Menü Konfiguration</string>
<string name="pref_view_menu_order_summary">Set the order of functions in the menu</string> <string name="pref_view_menu_order_summary">Legen Sie die Reihenfolge der Funktionen im Menü fest</string>
<string name="pref_notify_header">Benachrichtigungen</string> <string name="pref_notify_header">Benachrichtigungen</string>
<string name="pref_notify_header_other">Sonstiges</string> <string name="pref_notify_header_other">Sonstiges</string>
<string name="pref_notify_switch">Benachrichtigungen anzeigen</string> <string name="pref_notify_switch">Benachrichtigungen anzeigen</string>
@ -649,6 +665,8 @@
<string name="pref_other_grade_modifier_minus">Wert des Minus</string> <string name="pref_other_grade_modifier_minus">Wert des Minus</string>
<string name="pref_other_fill_message_content">Antwort mit Nachrichtenhistorie</string> <string name="pref_other_fill_message_content">Antwort mit Nachrichtenhistorie</string>
<string name="pref_other_optional_arithmetic_average">Arithmetisches Mittel anzeigen, wenn keine Gewichte angegeben sind</string> <string name="pref_other_optional_arithmetic_average">Arithmetisches Mittel anzeigen, wenn keine Gewichte angegeben sind</string>
<string name="pref_other_incognito_mode">Incognito mode</string>
<string name="pref_other_incognito_mode_summary">Do not inform about reading the message</string>
<string name="pref_ads_support_category_name">Unterstützung</string> <string name="pref_ads_support_category_name">Unterstützung</string>
<string name="pref_ads_privacy_policy">Datenschutz-Bestimmungen</string> <string name="pref_ads_privacy_policy">Datenschutz-Bestimmungen</string>
<string name="pref_ads_agreements">Vereinbarungen</string> <string name="pref_ads_agreements">Vereinbarungen</string>
@ -718,9 +736,18 @@
<string name="update_download_success_button">Neustart</string> <string name="update_download_success_button">Neustart</string>
<string name="update_failed">Update fehlgeschlagen! Wulkanowy funktioniert möglicherweise nicht richtig. Überlegen Sie die Aktualisierung</string> <string name="update_failed">Update fehlgeschlagen! Wulkanowy funktioniert möglicherweise nicht richtig. Überlegen Sie die Aktualisierung</string>
<!--Menu order--> <!--Menu order-->
<string name="menu_order_confirm_title">Application restart</string> <string name="menu_order_confirm_title">Neustart der Anwendung</string>
<string name="menu_order_confirm_content">The application must restart for the changes to be saved</string> <string name="menu_order_confirm_content">Die Anwendung muss neu gestartet werden, damit die Änderungen gespeichert werden</string>
<string name="menu_order_confirm_restart">Restart</string> <string name="menu_order_confirm_restart">Restart</string>
<!--Auth-->
<string name="auth_api_error">Authorization has been rejected. The data provided does not match the records in the secretary\'s office.</string>
<string name="auth_invalid_error">Invalid PESEL</string>
<string name="auth_pesel">PESEL</string>
<string name="auth_button">Authorize</string>
<string name="auth_success">Authorization completed successfully</string>
<string name="auth_title">Authorization</string>
<string name="auth_description">To operate the application, we need to confirm your identity. Please enter the student\'s PESEL &lt;b&gt;%1$s&lt;/b&gt; in the field below</string>
<string name="auth_button_skip">Skip for now</string>
<!--Errors--> <!--Errors-->
<string name="error_no_internet">Keine Internetverbindung</string> <string name="error_no_internet">Keine Internetverbindung</string>
<string name="error_invalid_device_datetime">Es ist ein Fehler aufgetreten. Überprüfen Sie Ihre Geräteuhr</string> <string name="error_invalid_device_datetime">Es ist ein Fehler aufgetreten. Überprüfen Sie Ihre Geräteuhr</string>

Some files were not shown because too many files have changed in this diff Show More