1
0

Compare commits

..

77 Commits
2.0.7 ... 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
132 changed files with 8035 additions and 626 deletions

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 129 versionCode 132
versionName "2.0.7" 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,16 +153,15 @@ android {
kapt { kapt {
correctErrorTypes true correctErrorTypes true
} }
ksp {
kotlin { arg("room.schemaLocation", "$projectDir/schemas".toString())
jvmToolchain(11)
} }
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.25d userFraction = 0.01d
updatePriority = 1 updatePriority = 1
enabled.set(false) enabled.set(false)
} }
@ -177,7 +173,7 @@ huaweiPublish {
buildFormat = "aab" buildFormat = "aab"
deployType = "publish" deployType = "publish"
releaseNotes = [ releaseNotes = [
new ru.cian.huawei.publish.ReleaseNote( new ReleaseNote(
"pl-PL", "pl-PL",
"$projectDir/src/main/play/release-notes/pl-PL/default.txt" "$projectDir/src/main/play/release-notes/pl-PL/default.txt"
) )
@ -189,29 +185,29 @@ 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.7.1" coroutines = "1.7.3"
} }
dependencies { dependencies {
implementation 'io.github.wulkanowy:sdk:2.0.7' 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.1" 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.1" 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.2" 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-beta02" 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"
@ -224,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"
@ -244,7 +240,7 @@ 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.4.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"
@ -252,17 +248,18 @@ dependencies {
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:32.1.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.1.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.10.0.301' 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"

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,39 +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.colorMessageHigh) to
context.getThemeAttrColor(R.attr.colorOnMessageHigh)
}
"MEDIUM" -> {
context.getThemeAttrColor(R.attr.colorMessageMedium) to Color.BLACK
}
else -> null to context.getThemeAttrColor(R.attr.colorOnSurface)
}
with(adminMessageViewHolder.binding) {
dashboardAdminMessageItemTitle.text = item.title
dashboardAdminMessageItemTitle.setTextColor(textColor)
dashboardAdminMessageItemDescription.text = item.content
dashboardAdminMessageItemDescription.setTextColor(textColor)
dashboardAdminMessageItemIcon.setColorFilter(textColor)
dashboardAdminMessageItemDismiss.isVisible = item.isDismissible
dashboardAdminMessageItemDismiss.setTextColor(textColor)
dashboardAdminMessageItemDismiss.setOnClickListener {
onAdminMessageDismissClickListener(item)
}
root.setCardBackgroundColor(backgroundColor?.let { ColorStateList.valueOf(it) })
item.destinationUrl?.let { url ->
root.setOnClickListener { onAdminMessageClickListener(url) }
}
}
}
private fun bindAdsViewHolder(adsViewHolder: AdsViewHolder, position: Int) { 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
@ -819,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,7 +1,5 @@
Wersja 2.0.7 Wersja 2.2.0
— poprawiliśmy wyświetlanie kilku rodzajów zmian w planie lekcji Dokonaliśmy drobnych usprawnień w obszarze logowania, które powinny pomóc Wam i nam
— dodaliśmy limit znaków w okienku usprawiedliwiania
— naprawiliśmy wyświetlanie frekwencji w szkołach, gdzie działa już system eduOne (ciągle jednak brak opcji usprawiedliwiania)
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

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

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

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>

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

@ -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,7 +55,8 @@
<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">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_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>
@ -68,7 +71,7 @@
<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>
@ -76,6 +79,12 @@
<string name="login_other_search_locations">Andere Suchorte</string> <string name="login_other_search_locations">Andere Suchorte</string>
<string name="login_no_active_student">Keine aktiven Schüler gefunden</string> <string name="login_no_active_student">Keine aktiven Schüler gefunden</string>
<string name="login_symbol_enter">Geben Sie ein anderes Symbol ein</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">Benachrichtigungen aktivieren</string> <string name="notifications_header_title">Benachrichtigungen aktivieren</string>
<string name="notifications_header_description">Aktivieren Sie Benachrichtigungen, damit Sie keine Nachricht vom Lehrer oder eine neue Klasse verpassen</string> <string name="notifications_header_description">Aktivieren Sie Benachrichtigungen, damit Sie keine Nachricht vom Lehrer oder eine neue Klasse verpassen</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>
@ -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>
@ -609,6 +624,7 @@
<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>
@ -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>

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>

View File

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<string-array name="app_theme_entries" tools:ignore="InconsistentArrays">
<item>Light</item>
<item>Dark</item>
<item>Black (AMOLED)</item>
</string-array>
<string-array name="app_language_entries">
<item>System language</item>
<item>Polski</item>
<item>English</item>
<item>Pусский</item>
<item>Українська</item>
<item>Deutsch</item>
<item>Čeština</item>
<item>Slovenčina</item>
</string-array>
<string-array name="services_interval_entries">
<item>15 minutes</item>
<item>30 minutes</item>
<item>1 hour</item>
<item>2 hours</item>
<item>6 hours</item>
<item>12 hours</item>
<item>24 hours</item>
</string-array>
<string-array name="grade_modifier_entries">
<item>0,00</item>
<item>0,25</item>
<item>0,33</item>
<item>0,5</item>
<item>0,75</item>
</string-array>
<string-array name="grade_sorting_mode_entries">
<item>Alphabetically</item>
<item>By date</item>
<item>By average</item>
</string-array>
<string-array name="grade_color_scheme_entries">
<item>Dzienniczek+</item>
<item>Wulkanowy</item>
<item>Grade colors in register</item>
</string-array>
<string-array name="default_expand_grade_entries">
<item>Up to 1 at once</item>
<item>Always expanded</item>
<item>Unlimited expansions</item>
</string-array>
<string-array name="grade_average_mode_entries">
<item>Average of grades only from selected semester</item>
<item>Average of averages from both semesters</item>
<item>Average of grades from the whole year</item>
</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">
<item>Lucky number</item>
<item>Unread messages</item>
<item>Attendance</item>
<item>Lessons</item>
<item>Grades</item>
<item>Homework</item>
<item>School announcements</item>
<item>Exams</item>
<item>Conferences</item>
</string-array>
</resources>

View File

@ -0,0 +1,764 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--Activity/Fragment title-->
<string name="login_title">Login</string>
<string name="main_title">Wulkanowy</string>
<string name="grade_title">Grades</string>
<string name="attendance_title">Attendance</string>
<string name="exam_title">Exams</string>
<string name="timetable_title">Timetable</string>
<string name="settings_title">Settings</string>
<string name="more_title">More</string>
<string name="about_title">About</string>
<string name="logviewer_title">Log viewer</string>
<string name="debug_title">Debug</string>
<string name="notification_debug_title">Notification debug</string>
<string name="contributors_title">Contributors</string>
<string name="license_title">Licenses</string>
<string name="message_title">Messages</string>
<string name="send_message_title">New message</string>
<string name="add_homework_title">New homework</string>
<string name="note_title">Notes and achievements</string>
<string name="homework_title">Homework</string>
<string name="account_title">Accounts manager</string>
<string name="account_quick_title">Select account</string>
<string name="account_details_title">Account details</string>
<string name="student_info_title">Student info</string>
<string name="dashboard_title">Dashboard</string>
<string name="notifications_center_title">Notifications center</string>
<string name="menu_order_title">Menu configuartion</string>
<!--Subtitles-->
<string name="grade_subtitle">Semester %1$d, %2$d/%3$d</string>
<!--Login-->
<string name="login_header_default">Sign in with the student or parent account</string>
<string name="login_header_symbol">Enter the symbol from the register page for account: &lt;b&gt;%1$s&lt;/b&gt;</string>
<string name="login_nickname_hint">Username</string>
<string name="login_email_hint">Email</string>
<string name="login_login_pesel_email_hint">Login, PESEL or e-mail</string>
<string name="login_password_hint">Password</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_scrapper">Scraper</string>
<string name="login_type_hybrid">Hybrid</string>
<string name="login_token_hint">Token</string>
<string name="login_pin_hint">PIN</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_invalid_password">Password too short</string>
<string name="login_incorrect_password_default">Login details are incorrect</string>
<string name="login_incorrect_password">%1$s. Make sure the correct UONET+ register variation is selected below</string>
<string name="login_invalid_pin">Invalid PIN</string>
<string name="login_invalid_token">Invalid token</string>
<string name="login_expired_token">Token expired</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_custom_email">Use the assigned login or email in @%1$s</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_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_select_student">Select students to log in to the application</string>
<string name="login_advanced">Other options</string>
<string name="login_advanced_warning_mobile_api">In this mode, a lucky number does not work, a class grade stats, summary of attendance, excuse for absence, completed lessons, school information and preview of the list of registered devices</string>
<string name="login_advanced_warning_scraper">This mode displays the same data as it appears on the register website</string>
<string name="login_advanced_warning_hybrid">The combination of the best features of the other two modes. It works faster than scraper and provides features not available in the Mobile API mode. It is in the experimental phase</string>
<string name="login_privacy_policy">Privacy policy</string>
<string name="login_contact_header">Trouble signing in? Contact us!</string>
<string name="login_contact_email">Email</string>
<string name="login_contact_discord">Discord</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_button">Reset password</string>
<string name="login_recover_title">Recover your account</string>
<string name="login_recover">Recover</string>
<string name="login_signed_in">Student is already signed in</string>
<string name="login_host_standard">Standard</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_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-->
<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_skip">Skip</string>
<string name="notifications_enable">Enable</string>
<!--Main-->
<string name="main_account_picker">Account manager</string>
<string name="main_log_in">Log in</string>
<string name="main_session_expired">Session expired</string>
<string name="main_session_relogin">Session expired, log in again</string>
<string name="main_support_title">Application support</string>
<string name="main_support_description">Do you like this app? Support its development by enabling non-invasive ads that you can disable at any time</string>
<string name="main_support_positive">Enable ads</string>
<!--Grade-->
<string name="grade_header">Grade</string>
<string name="grade_semester">Semester %d</string>
<string name="grade_switch_semester">Change semester</string>
<string name="grade_no_items">No grades</string>
<string name="grade_weight">Weight</string>
<string name="grade_weight_value">Weight: %s</string>
<string name="grade_comment">Comment</string>
<string name="grade_number_new_items">Number of new ratings: %1$d</string>
<string name="grade_average">Average: %1$.2f</string>
<string name="grade_points_sum">Points: %s</string>
<string name="grade_no_average">No average</string>
<string name="grade_summary_points">Total points</string>
<string name="grade_summary_final_grade">Final grade</string>
<string name="grade_summary_predicted_grade">Predicted grade</string>
<string name="grade_summary_calculated_average">Calculated average</string>
<string name="grade_summary_calculated_average_help_dialog_title">How does Calculated Average work?</string>
<string name="grade_summary_calculated_average_help_dialog_message">The Calculated Average is the arithmetic average calculated from the subjects averages. It allows you to know the approximate final average. It is calculated in a way selected by the user in the application settings. It is recommended that you choose the appropriate option. This is because the calculation of school averages differs. Additionally, if your school reports the average of the subjects on the Vulcan page, the application downloads them and does not calculate these averages. This can be changed by forcing the calculation of the average in the application settings.\n\n<b>Average of grades only from selected semester</b>:\n1. Calculating the weighted average for each subject in a given semester\n2.Adding calculated averages\n3. Calculation of the arithmetic average of the summed averages\n\n<b>Average of averages from both semesters</b>:\n1.Calculating the weighted average for each subject in semester 1 and 2\n2. Calculating the arithmetic average of the calculated averages for semesters 1 and 2 for each subject.\n3. Adding calculated averages\n4. Calculation of the arithmetic average of the summed averages\n\n<b>Average of grades from the whole year:</b>\n1. Calculating weighted average over the year for each subject. The final average in the 1st semester is irrelevant.\n2. Adding calculated averages\n3. Calculating the arithmetic average of summed averages</string>
<string name="grade_summary_final_average_help_dialog_title">How does the Final Average work?</string>
<string name="grade_summary_final_average_help_dialog_message">The Final Average is the arithmetic average calculated from all currently available final grades in the given semester.\n\nThe calculation scheme consists of the following steps:\n1. Summing up the final grades given by teachers\n2. Divide by the number of subjects that have already been graded</string>
<string name="grade_summary_final_average">Final average</string>
<string name="grade_summary_from_subjects">from %1$d of %2$d subjects</string>
<string name="grade_menu_summary">Summary</string>
<string name="grade_menu_statistics">Class</string>
<string name="grade_menu_read">Mark as read</string>
<string name="grade_statistics_partial">Partial</string>
<string name="grade_statistics_semester">Semester</string>
<string name="grade_statistics_points">Points</string>
<string name="grade_statistics_legend">Legend</string>
<string name="grade_statistics_class_average">Class average: %1$s</string>
<string name="grade_statistics_student_average">Your average: %1$s</string>
<string name="grade_statistics_student_grade">Your grade: %1$s</string>
<string name="grade_statistics_average_class">Class</string>
<string name="grade_statistics_average_student">Student</string>
<plurals name="grade_number_item">
<item quantity="one">%d grade</item>
<item quantity="other">%d grades</item>
</plurals>
<plurals name="grade_new_items">
<item quantity="one">New grade</item>
<item quantity="other">New grades</item>
</plurals>
<plurals name="grade_new_items_predicted">
<item quantity="one">New predicted grade</item>
<item quantity="other">New predicted grades</item>
</plurals>
<plurals name="grade_new_items_final">
<item quantity="one">New final grade</item>
<item quantity="other">New final grades</item>
</plurals>
<plurals name="grade_notify_new_items">
<item quantity="one">You received %1$d grade</item>
<item quantity="other">You received %1$d grades</item>
</plurals>
<plurals name="grade_notify_new_items_predicted">
<item quantity="one">You received %1$d predicted grade</item>
<item quantity="other">You received %1$d predicted grades</item>
</plurals>
<plurals name="grade_notify_new_items_final">
<item quantity="one">You received %1$d final grade</item>
<item quantity="other">You received %1$d final grades</item>
</plurals>
<!--Timetable-->
<string name="timetable_lesson">Lesson</string>
<string name="timetable_room">Room</string>
<string name="timetable_group">Group</string>
<string name="timetable_time">Hours</string>
<string name="timetable_changes">Changes</string>
<string name="timetable_no_items">No lessons this day</string>
<string name="timetable_minutes">%s min</string>
<string name="timetable_seconds">%s sec</string>
<string name="timetable_time_left">%1$s left</string>
<string name="timetable_time_until">in %1$s</string>
<string name="timetable_finished">Finished</string>
<string name="timetable_now">Now: %s</string>
<string name="timetable_next">Next: %s</string>
<string name="timetable_later">Later: %s</string>
<string name="timetable_notify_lesson">%1$s lesson %2$d - %3$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_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">
<item quantity="one">Timetable change</item>
<item quantity="other">Timetable changes</item>
</plurals>
<plurals name="timetable_notify_new_items">
<item quantity="one">%1$s - %2$d change in timetable</item>
<item quantity="other">%1$s - %2$d changes in timetable</item>
</plurals>
<plurals name="timetable_notify_new_items_group">
<item quantity="one">%1$d change in timetable</item>
<item quantity="other">%1$d changes in timetable</item>
</plurals>
<plurals name="timetable_number_item">
<item quantity="one">%d change</item>
<item quantity="other">%d changes</item>
</plurals>
<!--Completed lessons-->
<string name="completed_lessons_title">Completed lessons</string>
<string name="completed_lessons_button">Show completed lessons</string>
<string name="completed_lessons_no_items">No info about completed lessons</string>
<string name="completed_lessons_topic">Topic</string>
<string name="completed_lessons_absence">Absence</string>
<string name="completed_lessons_resources">Resources</string>
<!--Additional lessons-->
<string name="additional_lessons_title">Additional lessons</string>
<string name="additional_lessons_button">Show additional lessons</string>
<string name="additional_lessons_no_items">No info about additional lessons</string>
<string name="additional_lessons_add">New lesson</string>
<string name="additional_lessons_add_title">New additional lesson</string>
<string name="additional_lessons_add_success">Additional lesson added successfully</string>
<string name="additional_lessons_delete_success">Additional lesson deleted successfully</string>
<string name="additional_lessons_repeat">Repeat weekly</string>
<string name="additional_lessons_delete_title">Delete additional lesson</string>
<string name="additional_lessons_delete_one">Just this lesson</string>
<string name="additional_lessons_delete_series">All in the series</string>
<string name="additional_lessons_start">Start time</string>
<string name="additional_lessons_end">End time</string>
<string name="additional_lessons_end_time_error">End time must be greater than start time</string>
<!--Attendance-->
<string name="attendance_summary_button">Attendance summary</string>
<string name="attendance_absence_school">Absent for school reasons</string>
<string name="attendance_absence_excused">Excused absence</string>
<string name="attendance_absence_unexcused">Unexcused absence</string>
<string name="attendance_exemption">Exemption</string>
<string name="attendance_excused_lateness">Excused lateness</string>
<string name="attendance_unexcused_lateness">Unexcused lateness</string>
<string name="attendance_present">Present</string>
<string name="attendance_deleted">Deleted</string>
<string name="attendance_unknown">Unknown</string>
<string name="attendance_number">Number of lesson</string>
<string name="attendance_no_items">No entries</string>
<string name="attendance_excuse_dialog_reason">Absence reason (optional)</string>
<string name="attendance_excuse_dialog_submit">Send</string>
<string name="attendance_excuse_success">Absence excuse request sent successfully!</string>
<string name="attendance_excuse_no_selection">You must select at least one absence!</string>
<string name="attendance_excuse_title">Excuse</string>
<plurals name="attendance_notify_new_items_title">
<item quantity="one">New attendance</item>
<item quantity="other">New attendance</item>
</plurals>
<plurals name="attendance_notify_new_items">
<item quantity="one">%1$d new attendance</item>
<item quantity="other">%1$d attendance</item>
</plurals>
<plurals name="attendance_number_item">
<item quantity="one">%d attendance</item>
<item quantity="other">%d attendance</item>
</plurals>
<!--Attendance summary-->
<string name="attendance_summary_total">Total</string>
<!--Exam-->
<string name="exam_no_items">No exams this week</string>
<string name="exam_type">Type</string>
<string name="exam_entry_date">Entry date</string>
<plurals name="exam_notify_new_item_title">
<item quantity="one">New exam</item>
<item quantity="other">New exams</item>
</plurals>
<plurals name="exam_notify_new_item_content">
<item quantity="one">%d new exam</item>
<item quantity="other">%d new exams</item>
</plurals>
<plurals name="exam_number_item">
<item quantity="one">%d exam</item>
<item quantity="other">%d exams</item>
</plurals>
<!--Message-->
<string name="message_inbox">Inbox</string>
<string name="message_sent">Sent</string>
<string name="message_trash">Trash</string>
<string name="message_no_subject">(no subject)</string>
<string name="message_no_items">No messages</string>
<string name="message_from">From:</string>
<string name="message_to">To:</string>
<string name="message_date">Date: %1$s</string>
<string name="message_reply">Reply</string>
<string name="message_forward">Forward</string>
<string name="message_select_all">Select all</string>
<string name="message_unselect_all">Unselect all</string>
<string name="message_move_to_trash">Move to trash</string>
<string name="message_delete_forever">Delete permanently</string>
<string name="message_delete_success">Message deleted successfully</string>
<string name="message_mailbox_type_student">student</string>
<string name="message_mailbox_type_parent">parent</string>
<string name="message_mailbox_type_guardian">guardian</string>
<string name="message_mailbox_type_employee">employee</string>
<string name="message_share">Share</string>
<string name="message_print">Print</string>
<string name="message_subject">Subject</string>
<string name="message_content">Content</string>
<string name="message_send_successful">Message sent successfully</string>
<string name="message_not_exists">Message does not exist</string>
<string name="message_required_recipients">You need to choose at least 1 recipient</string>
<string name="message_content_min_length">The message content must be at least 3 characters</string>
<string name="message_chip_all_mailboxes">All mailboxes</string>
<string name="message_chip_only_unread">Only unread</string>
<string name="message_chip_only_with_attachments">Only with attachments</string>
<string name="message_read">Read: %s</string>
<string name="message_read_by">Read by: %1$d of %2$d people</string>
<plurals name="message_number_item">
<item quantity="one">%1$d message</item>
<item quantity="other">%1$d messages</item>
</plurals>
<plurals name="message_new_items">
<item quantity="one">New message</item>
<item quantity="other">New messages</item>
</plurals>
<string name="message_restore_dialog">Do you want to restore draft message?</string>
<string name="message_restore_dialog_with_recipients">Do you want to restore draft message with recipients: %s?</string>
<plurals name="message_notify_new_items">
<item quantity="one">You received %1$d message</item>
<item quantity="other">You received %1$d messages</item>
</plurals>
<plurals name="message_selected_messages_count">
<item quantity="one">%1$d selected</item>
<item quantity="other">%1$d selected</item>
</plurals>
<string name="message_messages_deleted">Messages deleted</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-->
<string name="note_no_items">No info about notes</string>
<string name="note_points">Points</string>
<plurals name="note_number_item">
<item quantity="one">%d note</item>
<item quantity="other">%d notes</item>
</plurals>
<plurals name="note_new_items">
<item quantity="one">New note</item>
<item quantity="other">New notes</item>
</plurals>
<plurals name="note_notify_new_items">
<item quantity="one">You received %1$d note</item>
<item quantity="other">You received %1$d notes</item>
</plurals>
<!--Praise-->
<plurals name="praise_number_item">
<item quantity="one">%d praise</item>
<item quantity="other">%d praises</item>
</plurals>
<plurals name="praise_new_items">
<item quantity="one">New praise</item>
<item quantity="other">New praises</item>
</plurals>
<plurals name="praise_notify_new_items">
<item quantity="one">You received %1$d praise</item>
<item quantity="other">You received %1$d praises</item>
</plurals>
<!--Neutral notes-->
<plurals name="neutral_note_number_item">
<item quantity="one">%d neutral note</item>
<item quantity="other">%d neutral notes</item>
</plurals>
<plurals name="neutral_note_new_items">
<item quantity="one">New neutral note</item>
<item quantity="other">New neutral notes</item>
</plurals>
<plurals name="neutral_note_notify_new_items">
<item quantity="one">You received %1$d neutral note</item>
<item quantity="other">You received %1$d neutral notes</item>
</plurals>
<!--Homework-->
<string name="homework_no_items">No info about homework</string>
<string name="homework_mark_as_done">Mark as done</string>
<string name="homework_mark_as_undone">Mark as undone</string>
<string name="homework_add">Add homework</string>
<string name="homework_add_success">Homework added successfully</string>
<string name="homework_delete_success">Homework deleted successfully</string>
<string name="homework_attachments">Attachments</string>
<plurals name="homework_notify_new_item_title">
<item quantity="one">New homework</item>
<item quantity="other">New homework</item>
</plurals>
<plurals name="homework_notify_new_item_content">
<item quantity="one">You received %d new homework</item>
<item quantity="other">You received %d new homework</item>
</plurals>
<plurals name="homework_number_item">
<item quantity="one">%d homework</item>
<item quantity="other">%d homework</item>
</plurals>
<!--Lucky number-->
<string name="lucky_number_title">Lucky number</string>
<string name="lucky_number_header">Today\'s lucky number is</string>
<string name="lucky_number_empty">No info about the lucky number</string>
<string name="lucky_number_notify_new_item_title">Lucky number for today</string>
<string name="lucky_number_notify_new_item">Today\'s lucky number is: %s</string>
<string name="lucky_number_history_button">Show history</string>
<!--Lucky number history-->
<string name="lucky_number_history_title">Lucky number history</string>
<string name="lucky_number_history_empty">No info about lucky numbers</string>
<!--Mobile devices-->
<string name="mobile_devices_title">Mobile devices</string>
<string name="mobile_devices_no_items">No devices</string>
<string name="mobile_devices_unregister">Deregister</string>
<string name="mobile_device_removed">Device removed</string>
<string name="mobile_device_qr">QR code</string>
<string name="mobile_device_token">Token</string>
<string name="mobile_device_symbol">Symbol</string>
<string name="mobile_device_pin">PIN</string>
<!--School and teachers-->
<string name="schoolandteachers_title">School and teachers</string>
<!--School-->
<string name="school_title">School</string>
<string name="school_no_info">No info about school</string>
<string name="school_name">School name</string>
<string name="school_address">School address</string>
<string name="school_telephone">Telephone</string>
<string name="school_headmaster">Name of headmaster</string>
<string name="school_pedagogue">Name of pedagogue</string>
<string name="school_address_button">Show on map</string>
<string name="school_telephone_button">Call</string>
<!--Teacher-->
<string name="teachers_title">Teachers</string>
<string name="teacher_no_items">No info about teachers</string>
<string name="teacher_no_subject">No subject</string>
<!--Conference-->
<string name="conferences_title">Conferences</string>
<string name="conference_no_items">No info about conferences</string>
<plurals name="conference_number_item">
<item quantity="one">%d conference</item>
<item quantity="other">%d conferences</item>
</plurals>
<plurals name="conference_notify_new_item_title">
<item quantity="one">New conference</item>
<item quantity="other">New conferences</item>
</plurals>
<plurals name="conference_notify_new_items">
<item quantity="one">You have %1$d new conference</item>
<item quantity="other">You have %1$d new conferences</item>
</plurals>
<string name="conferences_present">Present at conference</string>
<string name="conference_agenda">Agenda</string>
<string name="conference_place">Place</string>
<string name="conference_topic">Topic</string>
<!--Director information-->
<string name="school_announcement_title">School announcements</string>
<string name="school_announcement_no_items">No school announcements</string>
<plurals name="school_announcement_number_item">
<item quantity="one">%d school announcement</item>
<item quantity="other">%d school announcements</item>
</plurals>
<plurals name="school_announcement_notify_new_item_title">
<item quantity="one">New school announcement</item>
<item quantity="other">New school announcements</item>
</plurals>
<plurals name="school_announcement_notify_new_items">
<item quantity="one">You have %1$d new school announcement</item>
<item quantity="other">You have %1$d new school announcements</item>
</plurals>
<!--Account-->
<string name="account_add_new">Add account</string>
<string name="account_logout">Logout</string>
<string name="account_confirm">Do you want to log out this student?</string>
<string name="account_logout_student">Student logout</string>
<string name="account_type_student">Student account</string>
<string name="account_type_parent">Parent account</string>
<string name="account_details_edit">Edit data</string>
<string name="account_quick_manager">Accounts manager</string>
<string name="account_select_student">Select student</string>
<string name="account_family">Family</string>
<string name="account_contact">Contact</string>
<string name="account_address">Residence details</string>
<string name="account_personal_data">Personal information</string>
<!--About-->
<string name="about_version">App version</string>
<string name="about_contributor">Contributors</string>
<string name="about_contributor_summary">List of Wulkanowy developers</string>
<string name="about_feedback">Report a bug</string>
<string name="about_feedback_summary">Send a bug report via e-mail</string>
<string name="about_faq">FAQ</string>
<string name="about_faq_summary">Read Frequently Asked Questions</string>
<string name="about_discord">Discord server</string>
<string name="about_discord_summary">Join the Wulkanowy community</string>
<string name="about_facebook">Facebook fanpage</string>
<string name="about_twitter">Twitter page</string>
<string name="about_twitter_summary">Follow us on twitter</string>
<string name="about_facebook_summary">Like our facebook fanpage</string>
<string name="about_privacy">Privacy policy</string>
<string name="about_privacy_summary">Rules for collecting personal data</string>
<string name="about_system">System settings</string>
<string name="about_system_summary">Open system settings</string>
<string name="about_homepage">Homepage</string>
<string name="about_homepage_summary">Visit the website and help develop the application</string>
<string name="about_licenses">Licenses</string>
<string name="about_licenses_summary">Licenses of libraries used in the application</string>
<!--Licenses-->
<string name="license_dialog_title">License</string>
<!--Contributor-->
<string name="contributor_avatar_description">Avatar</string>
<string name="contributor_see_more">See more on GitHub</string>
<!--Student info-->
<string name="student_info_empty">No info about student or student family</string>
<string name="student_info_first_name">Name</string>
<string name="student_info_second_name">Second name</string>
<string name="student_info_gender">Gender</string>
<string name="student_info_polish_citizenship">Polish citizenship</string>
<string name="student_info_family_name">Family name</string>
<string name="student_info_parents_name">Mother\'s and father\'s names</string>
<string name="student_info_phone">Phone</string>
<string name="student_info_cellphone">Cellphone</string>
<string name="student_info_email">E-mail</string>
<string name="student_info_address">Address of residence</string>
<string name="student_info_registered_address">Address of registration</string>
<string name="student_info_correspondence_address">Correspondence address</string>
<string name="student_info_full_name">Surname and first name</string>
<string name="student_info_kinship">Degree of kinship</string>
<string name="student_info_guardian_address">Address</string>
<string name="student_info_phones">Phones</string>
<string name="student_info_male">Male</string>
<string name="student_info_female">Female</string>
<string name="student_info_last_name">Last name</string>
<string name="student_info_guardian">Guardian</string>
<!--Account edit-->
<string name="account_edit_nick_hint">Nick</string>
<string name="account_edit_header">Add nick</string>
<string name="account_edit_avatar_title">Choose avatar color</string>
<!--Log viewer-->
<string name="logviewer_share">Share logs</string>
<string name="logviewer_refresh">Refresh</string>
<!--Dashboard-->
<string name="dashboard_timetable_title">Lessons</string>
<string name="dashboard_timetable_title_tomorrow">(Tomorrow)</string>
<string name="dashboard_timetable_title_today_and_tomorrow">(Today and tomorrow)</string>
<string name="dashboard_timetable_first_lesson_title_moment">In a moment:</string>
<string name="dashboard_timetable_first_lesson_title_soon">Soon:</string>
<string name="dashboard_timetable_first_lesson_title_first">First:</string>
<string name="dashboard_timetable_first_lesson_title_now">Now:</string>
<string name="dashboard_timetable_second_lesson_value_end">End of lessons</string>
<string name="dashboard_timetable_second_lessons_title">Next:</string>
<string name="dashboard_timetable_third_title">Later:</string>
<plurals name="dashboard_timetable_third_value">
<item quantity="one">%1$d more lesson</item>
<item quantity="other">%1$d more lessons</item>
</plurals>
<string name="dashboard_timetable_third_time">until %1$s</string>
<string name="dashboard_timetable_no_lessons">No upcoming lessons</string>
<string name="dashboard_timetable_error">An error occurred while loading the lessons</string>
<string name="dashboard_homework_title">Homework</string>
<string name="dashboard_homework_no_homework">No homework to do</string>
<string name="dashboard_homework_error">An error occurred while loading the homework</string>
<plurals name="dashboard_homework_more">
<item quantity="one">%1$d more homework</item>
<item quantity="other">%1$d more homework</item>
</plurals>
<string name="dashboard_homework_time">due %1$s</string>
<string name="dashboard_grade_title">Last grades</string>
<string name="dashboard_grade_no_grade">No new grades</string>
<string name="dashboard_grade_error">An error occurred while loading the grades</string>
<string name="dashboard_announcements_title">School announcements</string>
<string name="dashboard_announcements_no_announcements">No current announcements</string>
<string name="dashboard_announcements_error">An error occurred while loading the announcements</string>
<plurals name="dashboard_announcements_more">
<item quantity="one">%1$d more announcement</item>
<item quantity="other">%1$d more announcements</item>
</plurals>
<string name="dashboard_exams_title">Exams</string>
<string name="dashboard_exams_no_exams">No upcoming exams</string>
<string name="dashboard_exams_error">An error occurred while loading the exams</string>
<plurals name="dashboard_exams_more">
<item quantity="one">%1$d more exam</item>
<item quantity="other">%1$d more exams</item>
</plurals>
<string name="dashboard_conferences_title">Conferences</string>
<string name="dashboard_conferences_no_conferences">No upcoming conferences</string>
<string name="dashboard_conferences_error">An error occurred while loading the conferences</string>
<plurals name="dashboard_conference_more">
<item quantity="one">%1$d more conference</item>
<item quantity="other">%1$d more conferences</item>
</plurals>
<string name="dashboard_horizontal_group_error">An error occurred while loading data</string>
<string name="dashboard_horizontal_group_no_data">None</string>
<!--Error dialog-->
<string name="dialog_error_check_update">Check for updates</string>
<string name="dialog_error_check_update_message">Before reporting a bug, check first if an update with the bug fix is available</string>
<!--Generic-->
<string name="all_content">Content</string>
<string name="all_retry">Retry</string>
<string name="all_description">Description</string>
<string name="all_no_description">No description</string>
<string name="all_teacher">Teacher</string>
<string name="all_date">Date</string>
<string name="all_entry_date">Entry date</string>
<string name="all_color">Color</string>
<string name="all_details">Details</string>
<string name="all_category">Category</string>
<string name="all_close">Close</string>
<string name="all_no_data">No data</string>
<string name="all_subject">Subject</string>
<string name="all_prev">Prev</string>
<string name="all_next">Next</string>
<string name="all_search">Search</string>
<string name="all_search_hint">Search…</string>
<string name="all_yes">Yes</string>
<string name="all_no">No</string>
<string name="all_save">Save</string>
<string name="all_title">Title</string>
<string name="all_add">Add</string>
<string name="all_copied">Copied</string>
<string name="all_undo">Undo</string>
<string name="all_change">Change</string>
<string name="all_add_to_calendar">Add to calendar</string>
<string name="all_cancel">Cancel</string>
<!--Timetable Widget-->
<string name="widget_timetable_no_items">No lessons</string>
<string name="widget_timetable_last_synchronization">Synchronized on %1$s at %2$s</string>
<string name="widget_timetable_theme_title">Choose theme</string>
<string name="widget_timetable_theme_light">Light</string>
<string name="widget_timetable_theme_dark">Dark</string>
<string name="widget_timetable_theme_system">System Theme</string>
<!--Preferences-->
<string name="pref_view_header">App</string>
<string name="pref_view_list">Default view</string>
<string name="pref_view_grade_average_mode">Calculated average options</string>
<string name="pref_view_grade_average_force_calc">Force average calculation by app</string>
<string name="pref_view_present">Show presence</string>
<string name="pref_view_app_theme">Theme</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_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_subjects_without_grades">Show subjects without grades</string>
<string name="pref_view_grade_color_scheme">Grades color scheme</string>
<string name="pref_view_grade_sorting_mode">Subjects sorting</string>
<string name="pref_view_app_language">Language</string>
<string name="pref_view_menu_order_title">Menu configuration</string>
<string name="pref_view_menu_order_summary">Set the order of functions in the menu</string>
<string name="pref_notify_header">Notifications</string>
<string name="pref_notify_header_other">Other</string>
<string name="pref_notify_switch">Show notifications</string>
<string name="pref_notify_upcoming_lessons_switch">Show upcoming lesson notifications</string>
<string name="pref_notify_upcoming_lessons_persistent_switch">Make upcoming lesson notification persistent</string>
<string name="pref_notify_upcoming_lessons_persistent_summary">Turn off when notification is not showing in your watch/band</string>
<string name="pref_notify_open_system_settings">Open system notification settings</string>
<string name="pref_notify_fix_sync_issues">Fix synchronization &amp; notifications issues</string>
<string name="pref_notify_fix_sync_issues_message">Your device may have data synchronization issues and with notifications.\n\nTo fix them, you need to add Wulkanowy to the autostart and turn off battery optimization/saving in the phone settings.</string>
<string name="pref_notify_debug_switch">Show debug notifications</string>
<string name="pref_notify_disabled_summary">Synchronization is disabled</string>
<string name="pref_notify_notifications_piggyback_header">Official app notifications</string>
<string name="pref_notify_notifications_piggyback">Capture official app notifications</string>
<string name="pref_notify_notifications_piggyback_cancel_original">Remove official app notifications after capture</string>
<string name="pref_notification_piggyback_popup_title">Capture notifications</string>
<string name="pref_notification_piggyback_popup_description">With this feature you can gain a substitute of push notifications like in the official app. All you need to do is allow Wulkanowy to receive all notifications in your system settings.\n\nHow it works?\nWhen you get a notification in Dziennik VULCAN, Wulkanowy will be notified (that\'s what these extra permissions are for) and will trigger a sync so that can send its own notification.\n\nFOR ADVANCED USERS ONLY</string>
<string name="pref_notification_exact_alarm_popup_title">Upcoming lesson notifications</string>
<string name="pref_notification_exact_alarm_popup_descriptions">You must allow the Wulkanowy app to set alarms and reminders in your system settings to use this feature.</string>
<string name="pref_notification_go_to_settings">Go to settings</string>
<string name="pref_services_header">Synchronization</string>
<string name="pref_services_switch">Automatic update</string>
<string name="pref_services_suspended">Suspended on holidays</string>
<string name="pref_services_interval">Updates interval</string>
<string name="pref_services_wifi">Wi-Fi only</string>
<string name="pref_services_force_sync">Sync now</string>
<string name="pref_services_message_sync_success">Synced!</string>
<string name="pref_services_message_sync_failed">Sync failed</string>
<string name="pref_services_sync_in_progress">Sync in progress</string>
<string name="pref_services_last_full_sync_date">Last full sync: %s</string>
<string name="pref_other_grade_modifier_plus">Value of the plus</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_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_privacy_policy">Privacy Policy</string>
<string name="pref_ads_agreements">Agreements</string>
<string name="pref_ads_consent">Consent to processing of data related to ads</string>
<string name="pref_ads_show_in_app">Show ads in app</string>
<string name="pref_ads_support">Watch single ad to support project</string>
<string name="pref_ads_privacy_title">Consent to data processing</string>
<string name="pref_ads_privacy_description">To view an advertisement you must agree to the data processing terms of our Privacy Policy</string>
<string name="pref_ads_privacy_agree">Agree</string>
<string name="pref_ads_privacy_link">Privacy policy</string>
<string name="pref_ads_loading">Ad is loading</string>
<string name="pref_ads_once_per_visit">Thank you for your support, come back later for more ads</string>
<string name="pref_ads_consent_title">Can we use your data to display ads?</string>
<string name="pref_ads_consent_description">You can change your choice anytime in the app settings. We may use your data to display ads tailored to you or, using less of your data, display non-personalized ads. Please see our Privacy Policy for details</string>
<string name="pref_ads_summary_personalized">Personalized ads</string>
<string name="pref_ads_summary_non_personalized">Non-personalized ads</string>
<string name="pref_ads_over_18_years_old">I am over 18 years old</string>
<string name="pref_ads_option_personalized">Yes, personalized ads</string>
<string name="pref_ads_option_non_personalized">Yes, non-personalized ads</string>
<string name="pref_settings_advanced_title">Advanced</string>
<string name="pref_settings_appearance_title">Appearance &amp; Behavior</string>
<string name="pref_settings_notifications_title">Notifications</string>
<string name="pref_settings_sync_title">Synchronization</string>
<string name="pref_settings_ads_title">Advertisements</string>
<string name="pref_grades_appearance_header">Grades</string>
<string name="pref_dashboard_appearance_header">Dashboard</string>
<string name="pref_dashboard_appearance_tiles_title">Tiles visibility</string>
<string name="pref_attendance_appearance_view">Attendance</string>
<string name="pref_timetable_appearance_view">Timetable</string>
<string name="pref_grades_advanced_header">Grades</string>
<string name="pref_counted_average_advanced_header">Calculated average</string>
<string name="pref_messages_advanced_header">Messages</string>
<string name="pref_appearance_category">Appearance &amp; Behavior</string>
<string name="pref_appearance_category_summary">Languages, themes, subjects sorting</string>
<string name="pref_notifications_category_summary">App notifications, fix problems</string>
<string name="pref_notifications_category">Notifications</string>
<string name="pref_sync_category">Synchronization</string>
<string name="pref_sync_category_summary">Automatic update, synchronization interval</string>
<string name="pref_advanced_category_summary">Plus and minus values, average calculation</string>
<string name="pref_advanced_category">Advanced</string>
<string name="pref_about_category_summary">App version, contributors, social portals</string>
<string name="pref_ads_category_summary">Displaying advertisements, project support</string>
<!--Notification Channels-->
<string name="channel_new_grades">New grades</string>
<string name="channel_new_homework">New homework</string>
<string name="channel_new_conference">New conferences</string>
<string name="channel_new_exam">New exams</string>
<string name="channel_lucky_number">Lucky number</string>
<string name="channel_new_message">New messages</string>
<string name="channel_new_notes">New notes</string>
<string name="channel_new_school_announcement">New school announcements</string>
<string name="channel_push">Push notifications</string>
<string name="channel_upcoming_lessons">Upcoming lessons</string>
<string name="channel_debug">Debug</string>
<string name="channel_change_timetable">Timetable change</string>
<string name="channel_new_attendance">New attendance</string>
<!--Colors-->
<string name="all_black">Black</string>
<string name="all_red">Red</string>
<string name="all_blue">Blue</string>
<string name="all_green">Green</string>
<string name="all_purple">Purple</string>
<string name="all_empty_color">No color</string>
<!--Update helper-->
<string name="update_download_started">Download of updates has started…</string>
<string name="update_download_success">An update has just been downloaded.</string>
<string name="update_download_success_button">Restart</string>
<string name="update_failed">Update failed! Wulkanowy may not function properly. Consider updating</string>
<!--Menu order-->
<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_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-->
<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_timeout">Connection to register failed. Servers can be overloaded. Please try again later</string>
<string name="error_login_failed">Loading data failed. Please try again later</string>
<string name="error_password_change_required">Register password change required</string>
<string name="error_service_unavailable">Maintenance underway UONET + register. Try again later</string>
<string name="error_unknown_uonet">Unknown UONET + register error. Try again later</string>
<string name="error_unknown_app">Unknown application error. Please try again later</string>
<string name="error_unknown">An unexpected error occurred</string>
<string name="error_feature_disabled">Feature disabled by your school</string>
<string name="error_feature_not_available">Feature not available. Login in a mode other than Mobile API</string>
<string name="error_field_required">This field is required</string>
</resources>

View File

@ -51,6 +51,11 @@
<item>Średnia ze średnich z obu semestrów</item> <item>Średnia ze średnich z obu semestrów</item>
<item>Średnia wszystkich ocen z całego roku</item> <item>Średnia wszystkich ocen z całego roku</item>
</string-array> </string-array>
<string-array name="timetable_show_gaps_entries">
<item>Nie pokauj</item>
<item>Tylko między lekcjami</item>
<item>Przed i między lekcjami</item>
</string-array>
<string-array name="dashboard_tile_entries"> <string-array name="dashboard_tile_entries">
<item>Szczęśliwy numerek</item> <item>Szczęśliwy numerek</item>
<item>Nieprzeczytane wiadomości</item> <item>Nieprzeczytane wiadomości</item>

View File

@ -37,12 +37,14 @@
<string name="login_login_pesel_email_hint">Login, PESEL lub adres e-mail</string> <string name="login_login_pesel_email_hint">Login, PESEL lub adres e-mail</string>
<string name="login_password_hint">Hasło</string> <string name="login_password_hint">Hasło</string>
<string name="login_host_hint">Odmiana dziennika UONET+</string> <string name="login_host_hint">Odmiana dziennika UONET+</string>
<string name="login_domain_suffix_hint">Niestandardowy sufiks domeny</string>
<string name="login_type_api">Mobilne API</string> <string name="login_type_api">Mobilne API</string>
<string name="login_type_scrapper">Scraper</string> <string name="login_type_scrapper">Scraper</string>
<string name="login_type_hybrid">Hybrydowe</string> <string name="login_type_hybrid">Hybrydowe</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">Np. \"lodz\" czy \"powiatjaroslawski\"</string>
<string name="login_sign_in">Zaloguj</string> <string name="login_sign_in">Zaloguj</string>
<string name="login_invalid_password">To hasło jest za krótkie</string> <string name="login_invalid_password">To hasło jest za krótkie</string>
<string name="login_incorrect_password_default">Dane logowania są niepoprawne</string> <string name="login_incorrect_password_default">Dane logowania są niepoprawne</string>
@ -53,7 +55,8 @@
<string name="login_invalid_email">Nieprawidłowy adres e-mail</string> <string name="login_invalid_email">Nieprawidłowy adres e-mail</string>
<string name="login_invalid_login">Użyj loginu zamiast adresu e-mail</string> <string name="login_invalid_login">Użyj loginu zamiast adresu e-mail</string>
<string name="login_invalid_custom_email">Użyj loginu lub adresu e-mail w @%1$s</string> <string name="login_invalid_custom_email">Użyj loginu lub adresu e-mail w @%1$s</string>
<string name="login_invalid_symbol">Nieprawidłowy symbol</string> <string name="login_invalid_symbol">Nieprawidłowy symbol. Jeśli nie możesz go znaleźć, skontaktuj się ze szkołą</string>
<string name="login_invalid_symbol_definitely">Nie zmyślaj! Jeśli nie możesz znaleźć symbolu, skontaktuj się ze szkołą</string>
<string name="login_incorrect_symbol">Nie znaleziono ucznia. Sprawdź poprawność symbolu i wybranej odmiany dziennika UONET+</string> <string name="login_incorrect_symbol">Nie znaleziono ucznia. Sprawdź poprawność symbolu i wybranej odmiany dziennika UONET+</string>
<string name="login_duplicate_student">Wybrany uczeń jest już zalogowany</string> <string name="login_duplicate_student">Wybrany uczeń jest już zalogowany</string>
<string name="login_symbol_helper">Symbol można znaleźć na stronie dziennika w&#160;<b>Uczeń</b>&#160;<b>Dostęp Mobilny</b>&#160;<b>Wygeneruj kod dostępu</b>.\n\nUpewnij się, że ustawiłeś odpowiednią odmianę dziennika w polu <b>Odmiana dziennika UONET+</b> na pierwszym ekranie logowania</string> <string name="login_symbol_helper">Symbol można znaleźć na stronie dziennika w&#160;<b>Uczeń</b>&#160;<b>Dostęp Mobilny</b>&#160;<b>Wygeneruj kod dostępu</b>.\n\nUpewnij się, że ustawiłeś odpowiednią odmianę dziennika w polu <b>Odmiana dziennika UONET+</b> na pierwszym ekranie logowania</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">Wyślij wiadomość e-mail</string> <string name="login_email_intent_title">Wyślij wiadomość e-mail</string>
<string name="login_recover_warning">Upewnij się, że została wybrana odpowiednia odmiana dziennika UONET+!</string> <string name="login_recover_warning">Upewnij się, że została wybrana odpowiednia odmiana dziennika UONET+!</string>
<string name="login_recover_button">Nie pamiętam hasła</string> <string name="login_recover_button">Zresetuj hasło</string>
<string name="login_recover_title">Przywróć swoje konto</string> <string name="login_recover_title">Przywróć swoje konto</string>
<string name="login_recover">Przywróć</string> <string name="login_recover">Przywróć</string>
<string name="login_signed_in">Uczeń jest już zalogowany</string> <string name="login_signed_in">Uczeń jest już zalogowany</string>
@ -76,6 +79,12 @@
<string name="login_other_search_locations">Inne lokalizacje wyszukiwania</string> <string name="login_other_search_locations">Inne lokalizacje wyszukiwania</string>
<string name="login_no_active_student">Nie znaleziono aktywnych uczniów</string> <string name="login_no_active_student">Nie znaleziono aktywnych uczniów</string>
<string name="login_symbol_enter">Wprowadź inny symbol</string> <string name="login_symbol_enter">Wprowadź inny symbol</string>
<string name="login_support_title">Uzyskaj pomoc</string>
<string name="login_support_school_hint">Pełna nazwa szkoły (wymagana)</string>
<string name="login_support_school_placeholder">Np. ZSTiO Jarosław lub SP nr 99 w Łodzi</string>
<string name="login_support_additional_hint">Dodatkowe informacje (po polsku) (nieobowiązkowo)</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">Wyślij</string>
<!--Notifications--> <!--Notifications-->
<string name="notifications_header_title">Włącz powiadomienia</string> <string name="notifications_header_title">Włącz powiadomienia</string>
<string name="notifications_header_description">Włącz powiadomienia, aby nie przegapić wiadomości od nauczyciela lub nowej oceny</string> <string name="notifications_header_description">Włącz powiadomienia, aby nie przegapić wiadomości od nauczyciela lub nowej oceny</string>
@ -184,6 +193,12 @@
<string name="timetable_notify_change_room">Zmiana sali z %1$s na %2$s</string> <string name="timetable_notify_change_room">Zmiana sali z %1$s na %2$s</string>
<string name="timetable_notify_change_teacher">Zmiana nauczyciela z %1$s na %2$s</string> <string name="timetable_notify_change_teacher">Zmiana nauczyciela z %1$s na %2$s</string>
<string name="timetable_notify_change_subject">Zmiana przedmiotu z %1$s na %2$s</string> <string name="timetable_notify_change_subject">Zmiana przedmiotu z %1$s na %2$s</string>
<plurals name="timetable_no_lesson">
<item quantity="one">Brak lekcji</item>
<item quantity="few">Brak lekcji</item>
<item quantity="many">Brak lekcji</item>
<item quantity="other">Brak lekcji</item>
</plurals>
<plurals name="timetable_notify_new_items_title"> <plurals name="timetable_notify_new_items_title">
<item quantity="one">Zmiana planu lekcji</item> <item quantity="one">Zmiana planu lekcji</item>
<item quantity="few">Zmiany planu lekcji</item> <item quantity="few">Zmiany planu lekcji</item>
@ -351,6 +366,8 @@
</plurals> </plurals>
<string name="message_messages_deleted">Wiadomości zostały usunięte</string> <string name="message_messages_deleted">Wiadomości zostały usunięte</string>
<string name="message_mailbox_chooser_title">Wybierz skrzynkę</string> <string name="message_mailbox_chooser_title">Wybierz skrzynkę</string>
<string name="message_incognito_mode_on">Tryb incognito jest włączony</string>
<string name="message_incognito_description">Dzięki trybowi incognito nadawca nie zobaczy, że przeczytałeś tę wiadomość</string>
<!--Note--> <!--Note-->
<string name="note_no_items">Brak informacji o uwagach</string> <string name="note_no_items">Brak informacji o uwagach</string>
<string name="note_points">Punkty</string> <string name="note_points">Punkty</string>
@ -697,6 +714,7 @@
<string name="pref_view_expand_grade">Rozwijanie ocen</string> <string name="pref_view_expand_grade">Rozwijanie ocen</string>
<string name="pref_view_timetable_show_timers">Oznaczaj bieżącą lekcję</string> <string name="pref_view_timetable_show_timers">Oznaczaj bieżącą lekcję</string>
<string name="pref_view_timetable_show_groups">Pokazuj grupę obok przedmiotu</string> <string name="pref_view_timetable_show_groups">Pokazuj grupę obok przedmiotu</string>
<string name="pref_view_timetable_show_gaps">Pokazuj puste kafelki gdzie nie ma lekcji</string>
<string name="pref_view_grade_statistics_list">Pokazuj listę wykresów w ocenach klasy</string> <string name="pref_view_grade_statistics_list">Pokazuj listę wykresów w ocenach klasy</string>
<string name="pref_view_subjects_without_grades">Pokazuj przedmioty bez ocen</string> <string name="pref_view_subjects_without_grades">Pokazuj przedmioty bez ocen</string>
<string name="pref_view_grade_color_scheme">Schemat kolorów ocen</string> <string name="pref_view_grade_color_scheme">Schemat kolorów ocen</string>
@ -737,6 +755,8 @@
<string name="pref_other_grade_modifier_minus">Wartość minusa</string> <string name="pref_other_grade_modifier_minus">Wartość minusa</string>
<string name="pref_other_fill_message_content">Odpowiadaj z historią wiadomości</string> <string name="pref_other_fill_message_content">Odpowiadaj z historią wiadomości</string>
<string name="pref_other_optional_arithmetic_average">Licz średnią arytmetyczną, gdy żadna ocena nie ma wagi</string> <string name="pref_other_optional_arithmetic_average">Licz średnią arytmetyczną, gdy żadna ocena nie ma wagi</string>
<string name="pref_other_incognito_mode">Tryb incognito</string>
<string name="pref_other_incognito_mode_summary">Nie informuj o przeczytaniu wiadomości</string>
<string name="pref_ads_support_category_name">Wsparcie</string> <string name="pref_ads_support_category_name">Wsparcie</string>
<string name="pref_ads_privacy_policy">Polityka prywatności</string> <string name="pref_ads_privacy_policy">Polityka prywatności</string>
<string name="pref_ads_agreements">Zgody</string> <string name="pref_ads_agreements">Zgody</string>

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