1
0

Compare commits

...

122 Commits
2.0.5 ... 2.2.3

Author SHA1 Message Date
387ff1cba7 Merge branch 'release/2.2.3' 2023-10-26 18:31:56 +02:00
eef3464d0b Version 2.2.3 2023-10-26 18:31:51 +02:00
61297a01c7 New Crowdin updates (#2334) 2023-10-26 14:01:45 +02:00
762d4b1393 Timetable timers fixes (#2333) 2023-10-26 10:06:54 +02:00
2e86b67eec Merge branch 'release/2.2.2' into develop 2023-10-23 20:02:32 +02:00
6071b7571b Merge branch 'release/2.2.2' 2023-10-23 20:02:25 +02:00
fcea2218b5 Version 2.2.2 2023-10-23 19:56:46 +02:00
a4a191700e Bump com.google.firebase:firebase-bom from 32.3.1 to 32.4.0 (#2331) 2023-10-23 17:28:38 +00:00
3d76d41b55 Bump com.squareup.okhttp3:logging-interceptor from 4.11.0 to 4.12.0 (#2330) 2023-10-23 16:49:58 +00:00
0e1c20a952 Bump room from 2.5.2 to 2.6.0 (#2329) 2023-10-23 16:48:23 +00:00
5d14ee7f4e Bump androidx.recyclerview:recyclerview from 1.3.1 to 1.3.2 (#2332) 2023-10-23 16:47:03 +00:00
83527d91f3 Allow direct access to weekend from day navigation when there is any lesson during weekend (#2326) 2023-10-23 13:05:46 +02:00
9d62410530 Sort teachers by name in school and teachers tab (#2327) 2023-10-23 13:05:05 +02:00
5dffbdadfa Points statistics improvements (#2328) 2023-10-23 13:04:42 +02:00
516922d5aa Bump org.sonarsource.scanner.gradle:sonarqube-gradle-plugin (#2324) 2023-10-14 19:18:28 +00:00
9098e74065 Bump com.google.android.material:material from 1.9.0 to 1.10.0 (#2325) 2023-10-14 19:17:56 +00:00
2f5577cc54 Update SDK to 34 (#2322) 2023-10-06 10:07:55 +02:00
3272c38356 Merge branch 'release/2.2.1' into develop 2023-10-03 01:14:25 +02:00
bcd305bef3 Merge branch 'release/2.2.1' 2023-10-03 01:14:17 +02:00
fc5ad16cb7 Version 2.2.1 2023-10-03 01:14:10 +02:00
c8332a0642 Bump org.sonarsource.scanner.gradle:sonarqube-gradle-plugin (#2321) 2023-10-02 22:49:26 +00:00
3212efe21e Bump com.android.tools.build:gradle from 8.1.1 to 8.1.2 (#2320) 2023-10-02 22:49:05 +00:00
693ce8217d New Crowdin updates (#2313) 2023-10-03 00:48:37 +02:00
2cdd322ed4 Add missing class_id colum in JOIN clause of students with semesters (#2317) 2023-10-02 12:22:02 +02:00
c04b3e40d2 Add negative e-mail validation in school input on support dialog (#2315) 2023-10-02 12:21:04 +02:00
d1d665bbdf Fix student auto selection if there is already active some students logged (#2314) 2023-10-02 12:20:34 +02:00
d70568c446 Merge branch 'release/2.2.0' into develop 2023-09-26 23:13:46 +02:00
1d8378e136 Merge branch 'release/2.2.0' 2023-09-26 23:13:40 +02:00
4a2bf539f0 Version 2.2.0 2023-09-26 23:13:32 +02:00
4d085f8266 New Crowdin updates (#2311) 2023-09-26 21:04:27 +00:00
fca69e7234 Add form dialog to login e-mail support (#2306) 2023-09-26 22:27:08 +02:00
711de0f77f Fix average calculation when there is no real semesters available (#2310) 2023-09-26 22:26:19 +02:00
58d5196ac9 Improve symbol input field (#2312) 2023-09-26 22:25:23 +02:00
26a95ecb99 Auto select students for login (#2307)
* Auto select students for login

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

* Don't hide first password reset button

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

View File

@ -13,7 +13,7 @@ jobs:
environment: google-play
steps:
- uses: actions/checkout@v3
- uses: actions/setup-java@v2
- uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: 17
@ -49,7 +49,7 @@ jobs:
environment: app-gallery
steps:
- uses: actions/checkout@v3
- uses: actions/setup-java@v2
- uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: 17

View File

@ -19,7 +19,7 @@ jobs:
environment: app-center
steps:
- uses: actions/checkout@v3
- uses: actions/setup-java@v2
- uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: 17
@ -89,7 +89,7 @@ jobs:
if: github.event_name != 'pull_request_target'
steps:
- uses: actions/checkout@v3
- uses: actions/setup-java@v2
- uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: 17

View File

@ -19,7 +19,7 @@ jobs:
- uses: fkirc/skip-duplicate-actions@master
- uses: actions/checkout@v3
- uses: gradle/wrapper-validation-action@v1
- uses: actions/setup-java@v2
- uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: 17
@ -45,7 +45,7 @@ jobs:
- uses: fkirc/skip-duplicate-actions@master
- uses: actions/checkout@v3
- uses: gradle/wrapper-validation-action@v1
- uses: actions/setup-java@v2
- uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: 17
@ -71,7 +71,7 @@ jobs:
- uses: fkirc/skip-duplicate-actions@master
- uses: actions/checkout@v3
- uses: gradle/wrapper-validation-action@v1
- uses: actions/setup-java@v2
- uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: 17

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

View File

@ -1,16 +1,16 @@
apply plugin: "jacoco"
jacoco {
toolVersion "0.8.7"
toolVersion "0.8.10"
reportsDirectory.set(file("$buildDir/reports"))
}
tasks.withType(Test) {
tasks.withType(Test).configureEach {
jacoco.includeNoLocationClasses = true
jacoco.excludes = ['jdk.internal.*']
}
task jacocoTestReport(type: JacocoReport) {
tasks.register('jacocoTestReport', JacocoReport) {
group = "Reporting"
description = "Generate Jacoco coverage reports"
@ -33,19 +33,19 @@ task jacocoTestReport(type: JacocoReport) {
'**/*_Factory.*']
classDirectories.setFrom(fileTree(
dir: "$buildDir/intermediates/classes/debug",
excludes: excludes
dir: "$buildDir/intermediates/classes/debug",
excludes: excludes
) + fileTree(
dir: "$buildDir/tmp/kotlin-classes/fdroidDebug",
excludes: excludes
dir: "$buildDir/tmp/kotlin-classes/fdroidDebug",
excludes: excludes
))
sourceDirectories.setFrom(files([
"src/main/java",
"src/fdroid/java"
"src/main/java",
"src/fdroid/java"
]))
executionData.setFrom(fileTree(
dir: project.projectDir,
includes: ["**/*.exec", "**/*.ec"]
dir: project.projectDir,
includes: ["**/*.exec", "**/*.ec"]
))
}

View File

@ -26,6 +26,7 @@
#Config for Material Components
-keep class com.google.android.material.tabs.** { *; }
#Config for HMS SDK
-keepattributes *Annotation*
-keepattributes Exceptions
@ -35,3 +36,7 @@
-keep class com.huawei.hianalytics.**{*;}
-keep class com.huawei.updatesdk.**{*;}
-keep class com.huawei.hms.**{*;}
#Config for Wulkanowy SDK
-keep,allowobfuscation,allowshrinking class retrofit2.Response

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

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

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.",
"githubUsername": "Pengwius"
},
{
"displayName": "Antoni Paduch",
"githubUsername": "janAte1"
}
]

View File

@ -6,7 +6,7 @@ import androidx.hilt.work.HiltWorkerFactory
import androidx.work.Configuration
import com.yariksoffice.lingver.Lingver
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.ui.base.ThemeManager
import io.github.wulkanowy.utils.*

View File

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

View File

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

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

View File

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

View File

@ -1,6 +1,7 @@
package io.github.wulkanowy.data.db.dao
import androidx.room.*
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentName
import io.github.wulkanowy.data.db.entities.StudentNickAndAvatar
@ -33,12 +34,12 @@ abstract class StudentDao {
abstract suspend fun loadAll(): List<Student>
@Transaction
@Query("SELECT * FROM Students")
abstract suspend fun loadStudentsWithSemesters(): List<StudentWithSemesters>
@Query("SELECT * FROM Students JOIN Semesters ON Students.student_id = Semesters.student_id AND Students.class_id = Semesters.class_id")
abstract suspend fun loadStudentsWithSemesters(): Map<Student, List<Semester>>
@Transaction
@Query("SELECT * FROM Students WHERE id = :id")
abstract suspend fun loadStudentWithSemestersById(id: Long): StudentWithSemesters?
@Query("SELECT * FROM Students JOIN Semesters ON Students.student_id = Semesters.student_id AND Students.class_id = Semesters.class_id WHERE Students.id = :id")
abstract suspend fun loadStudentWithSemestersById(id: Long): Map<Student, List<Semester>>
@Query("UPDATE Students SET is_current = 1 WHERE id = :id")
abstract suspend fun updateCurrent(id: Long)

View File

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

View File

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

View File

@ -1,13 +1,8 @@
package io.github.wulkanowy.data.db.entities
import androidx.room.Embedded
import androidx.room.Relation
import java.io.Serializable
data class StudentWithSemesters(
@Embedded
val student: Student,
@Relation(parentColumn = "student_id", entityColumn = "student_id")
val semesters: List<Semester>
) : Serializable

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

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
import io.github.wulkanowy.data.Resource
import io.github.wulkanowy.data.api.AdminMessageService
import io.github.wulkanowy.data.db.dao.AdminMessageDao
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.AdminMessage
import io.github.wulkanowy.data.networkBoundResource
import io.github.wulkanowy.utils.AppInfo
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.sync.Mutex
import javax.inject.Inject
import javax.inject.Singleton
@ -13,34 +14,20 @@ import javax.inject.Singleton
class AdminMessageRepository @Inject constructor(
private val adminMessageService: AdminMessageService,
private val adminMessageDao: AdminMessageDao,
private val appInfo: AppInfo
) {
private val saveFetchResultMutex = Mutex()
suspend fun getAdminMessages(student: Student) = networkBoundResource(
mutex = saveFetchResultMutex,
isResultEmpty = { it == null },
query = { adminMessageDao.loadAll() },
fetch = { adminMessageService.getAdminMessages() },
shouldFetch = { true },
saveFetchResult = { oldItems, newItems ->
adminMessageDao.removeOldAndSaveNew(oldItems, newItems)
},
showSavedOnLoading = false,
mapResult = { adminMessages ->
adminMessages.filter { adminMessage ->
val isCorrectRegister = adminMessage.targetRegisterHost?.let {
student.scrapperBaseUrl.contains(it, true)
} ?: true
val isCorrectFlavor =
adminMessage.targetFlavor?.equals(appInfo.buildFlavor, true) ?: true
val isCorrectMaxVersion =
adminMessage.versionMax?.let { it >= appInfo.versionCode } ?: true
val isCorrectMinVersion =
adminMessage.versionMin?.let { it <= appInfo.versionCode } ?: true
isCorrectRegister && isCorrectFlavor && isCorrectMaxVersion && isCorrectMinVersion
}.maxByOrNull { it.id }
}
)
fun getAdminMessages(): Flow<Resource<List<AdminMessage>>> =
networkBoundResource(
mutex = saveFetchResultMutex,
isResultEmpty = { false },
query = { adminMessageDao.loadAll() },
fetch = { adminMessageService.getAdminMessages() },
shouldFetch = { true },
saveFetchResult = { oldItems, newItems ->
adminMessageDao.removeOldAndSaveNew(oldItems, newItems)
},
showSavedOnLoading = false,
)
}

View File

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

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 kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.time.Instant
@ -195,10 +194,12 @@ class PreferencesRepository @Inject constructor(
)
)
val showTimetableTimers: Boolean
get() = getBoolean(
R.string.pref_key_timetable_show_timers,
R.bool.pref_default_timetable_show_timers
val showTimetableGaps: TimetableGapsMode
get() = TimetableGapsMode.getByValue(
getString(
R.string.pref_key_timetable_show_gaps,
R.string.pref_default_timetable_show_gaps
)
)
val showSubjectsWithoutGrades: Boolean
@ -343,6 +344,12 @@ class PreferencesRepository @Inject constructor(
)
}
var isIncognitoMode: Boolean
get() = getBoolean(R.string.pref_key_incognito_moge, R.bool.pref_default_incognito_mode)
set(value) = sharedPref.edit {
putBoolean(context.getString(R.string.pref_key_incognito_moge), value)
}
var installationId: String
get() = sharedPref.getString(PREF_KEY_INSTALLATION_ID, null).orEmpty()
private set(value) = sharedPref.edit { putString(PREF_KEY_INSTALLATION_ID, value) }

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

View File

@ -43,22 +43,14 @@ class StudentRepository @Inject constructor(
.getStudentsFromHebe(token, pin, symbol, "")
.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(
email: String,
password: String,
scrapperBaseUrl: String,
domainSuffix: String,
symbol: String
): RegisterUser = sdk
.getUserSubjectsFromScrapper(email, password, scrapperBaseUrl, symbol)
.getUserSubjectsFromScrapper(email, password, scrapperBaseUrl, domainSuffix, symbol)
.mapToPojo(password)
suspend fun getStudentsHybrid(
@ -70,20 +62,28 @@ class StudentRepository @Inject constructor(
.getStudentsHybrid(email, password, scrapperBaseUrl, "", symbol)
.mapToPojo(password)
suspend fun getSavedStudents(decryptPass: Boolean = true) =
studentDb.loadStudentsWithSemesters()
.map {
it.apply {
suspend fun getSavedStudents(decryptPass: Boolean = true): List<StudentWithSemesters> {
return studentDb.loadStudentsWithSemesters().map { (student, semesters) ->
StudentWithSemesters(
student = student.apply {
if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE) {
student.password = withContext(dispatchers.io) {
decrypt(student.password)
}
}
}
}
},
semesters = semesters,
)
}
}
suspend fun getSavedStudentById(id: Long, decryptPass: Boolean = true) =
studentDb.loadStudentWithSemestersById(id)?.apply {
suspend fun getSavedStudentById(id: Long, decryptPass: Boolean = true): StudentWithSemesters? =
studentDb.loadStudentWithSemestersById(id).let { res ->
StudentWithSemesters(
student = res.keys.firstOrNull() ?: return null,
semesters = res.values.first(),
)
}.apply {
if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE) {
student.password = withContext(dispatchers.io) {
decrypt(student.password)

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

View File

@ -148,6 +148,10 @@ class AttendanceFragment : BaseFragment<FragmentAttendanceBinding>(R.layout.frag
binding.attendanceNavDate.text = date
}
override fun showNavigation(show: Boolean) {
binding.attendanceNavContainer.isVisible = show
}
override fun clearData() {
with(attendanceAdapter) {
items = emptyList()
@ -281,7 +285,9 @@ class AttendanceFragment : BaseFragment<FragmentAttendanceBinding>(R.layout.frag
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putLong(SAVED_DATE_KEY, presenter.currentDate.toEpochDay())
presenter.currentDate?.let {
outState.putLong(SAVED_DATE_KEY, it.toEpochDay())
}
}
override fun onDestroyView() {

View File

@ -3,10 +3,14 @@ package io.github.wulkanowy.ui.modules.attendance
import android.annotation.SuppressLint
import io.github.wulkanowy.data.*
import io.github.wulkanowy.data.db.entities.Attendance
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.Timetable
import io.github.wulkanowy.data.repositories.AttendanceRepository
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.SemesterRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.repositories.TimetableRepository
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.utils.*
@ -14,6 +18,7 @@ import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.onEach
import timber.log.Timber
import java.time.DayOfWeek
import java.time.LocalDate
import java.time.LocalDate.now
import java.time.LocalDate.ofEpochDay
@ -28,9 +33,10 @@ class AttendancePresenter @Inject constructor(
private val analytics: AnalyticsHelper
) : BasePresenter<AttendanceView>(errorHandler, studentRepository) {
private var baseDate: LocalDate = now().previousOrSameSchoolDay
private var initialDate: LocalDate? = null
private var isWeekendHasLessons: Boolean = false
lateinit var currentDate: LocalDate
var currentDate: LocalDate? = null
private set
private lateinit var lastError: Throwable
@ -44,27 +50,34 @@ class AttendancePresenter @Inject constructor(
view.initView()
Timber.i("Attendance view was initialized")
errorHandler.showErrorMessage = ::showErrorViewOnError
reloadView(ofEpochDay(date ?: baseDate.toEpochDay()))
currentDate = date?.let(::ofEpochDay)
loadData()
if (currentDate.isHolidays) setBaseDateOnHolidays()
}
fun onPreviousDay() {
val date = if (isWeekendHasLessons) {
currentDate?.minusDays(1)
} else currentDate?.previousSchoolDay
view?.finishActionMode()
attendanceToExcuseList.clear()
reloadView(currentDate.previousSchoolDay)
reloadView(date ?: return)
loadData()
}
fun onNextDay() {
val date = if (isWeekendHasLessons) {
currentDate?.plusDays(1)
} else currentDate?.nextSchoolDay
view?.finishActionMode()
attendanceToExcuseList.clear()
reloadView(currentDate.nextSchoolDay)
reloadView(date ?: return)
loadData()
}
fun onPickDate() {
view?.showDatePickerDialog(currentDate)
view?.showDatePickerDialog(currentDate ?: return)
}
fun onDateSet(year: Int, month: Int, day: Int) {
@ -93,10 +106,8 @@ class AttendancePresenter @Inject constructor(
Timber.i("Attendance view is reselected")
view?.let { view ->
if (view.currentStackSize == 1) {
baseDate = now().previousOrSameSchoolDay
if (currentDate != baseDate) {
reloadView(baseDate)
if (currentDate != initialDate) {
reloadView(initialDate ?: return)
loadData()
} else if (!view.isViewEmpty) {
view.resetView()
@ -188,19 +199,6 @@ class AttendancePresenter @Inject constructor(
return true
}
private fun setBaseDateOnHolidays() {
flow {
val student = studentRepository.getCurrentStudent()
emit(semesterRepository.getCurrentSemester(student))
}.catch {
Timber.i("Loading semester result: An exception occurred")
}.onEach {
baseDate = baseDate.getLastSchoolDayIfHoliday(it.schoolYear)
currentDate = baseDate
reloadNavigation()
}.launch("holidays")
}
private fun loadData(forceRefresh: Boolean = false) {
Timber.i("Loading attendance data started")
@ -211,11 +209,13 @@ class AttendancePresenter @Inject constructor(
isParent = student.isParent
val semester = semesterRepository.getCurrentSemester(student)
checkInitialAndCurrentDate(student, semester)
attendanceRepository.getAttendance(
student = student,
semester = semester,
start = currentDate,
end = currentDate,
start = currentDate ?: now(),
end = currentDate ?: now(),
forceRefresh = forceRefresh
)
}
@ -231,6 +231,8 @@ class AttendancePresenter @Inject constructor(
}.sortedBy { item -> item.number }
}
.onResourceData {
isWeekendHasLessons = isWeekendHasLessons || isWeekendHasLessons(it)
view?.run {
enableSwipe(true)
showProgress(false)
@ -238,6 +240,7 @@ class AttendancePresenter @Inject constructor(
showEmpty(it.isEmpty())
showContent(it.isNotEmpty())
updateData(it)
reloadNavigation()
}
}
.onResourceIntermediate { view?.showRefresh(true) }
@ -263,6 +266,43 @@ class AttendancePresenter @Inject constructor(
.launch()
}
private suspend fun checkInitialAndCurrentDate(student: Student, semester: Semester) {
if (initialDate == null) {
val lessons = attendanceRepository.getAttendance(
student = student,
semester = semester,
start = now().monday,
end = now().sunday,
forceRefresh = false,
).toFirstResult().dataOrNull.orEmpty()
isWeekendHasLessons = isWeekendHasLessons(lessons)
initialDate = getInitialDate(semester)
}
if (currentDate == null) {
currentDate = initialDate
}
}
private fun isWeekendHasLessons(
lessons: List<Attendance>,
): Boolean = lessons.any {
it.date.dayOfWeek in listOf(
DayOfWeek.SATURDAY,
DayOfWeek.SUNDAY,
)
}
private fun getInitialDate(semester: Semester): LocalDate {
val now = now()
return when {
now.isHolidays -> now.getLastSchoolDayIfHoliday(semester.schoolYear)
isWeekendHasLessons -> now
else -> now.previousOrSameSchoolDay
}
}
private fun excuseAbsence(reason: String?, toExcuseList: List<Attendance>) {
resourceFlow {
val student = studentRepository.getCurrentStudent()
@ -311,7 +351,7 @@ class AttendancePresenter @Inject constructor(
private fun reloadView(date: LocalDate) {
currentDate = date
Timber.i("Reload attendance view with the date ${currentDate.toFormattedString()}")
Timber.i("Reload attendance view with the date ${currentDate?.toFormattedString()}")
view?.apply {
showProgress(true)
enableSwipe(false)
@ -326,10 +366,13 @@ class AttendancePresenter @Inject constructor(
@SuppressLint("DefaultLocale")
private fun reloadNavigation() {
val currentDate = currentDate ?: return
view?.apply {
showPreButton(!currentDate.minusDays(1).isHolidays)
showNextButton(!currentDate.plusDays(1).isHolidays)
updateNavigationDay(currentDate.toFormattedString("EEEE, dd.MM").capitalise())
showNavigation(true)
}
}
}

View File

@ -40,6 +40,8 @@ interface AttendanceView : BaseView {
fun showContent(show: Boolean)
fun showNavigation(show: Boolean)
fun showPreButton(show: Boolean)
fun showNextButton(show: Boolean)

View File

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

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

View File

@ -1,8 +1,6 @@
package io.github.wulkanowy.ui.modules.dashboard.adapters
import android.annotation.SuppressLint
import android.content.res.ColorStateList
import android.graphics.Color
import android.graphics.Typeface
import android.os.Handler
import android.os.Looper
@ -24,6 +22,7 @@ import io.github.wulkanowy.data.db.entities.TimetableHeader
import io.github.wulkanowy.data.enums.GradeColorTheme
import io.github.wulkanowy.databinding.*
import io.github.wulkanowy.ui.modules.dashboard.DashboardItem
import io.github.wulkanowy.ui.modules.dashboard.viewholders.AdminMessageViewHolder
import io.github.wulkanowy.utils.*
import timber.log.Timber
import java.time.Duration
@ -109,7 +108,9 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
ItemDashboardConferencesBinding.inflate(inflater, parent, false)
)
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(
ItemDashboardAdsBinding.inflate(inflater, parent, false)
@ -128,7 +129,7 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
is AnnouncementsViewHolder -> bindAnnouncementsViewHolder(holder, position)
is ExamsViewHolder -> bindExamsViewHolder(holder, position)
is ConferencesViewHolder -> bindConferencesViewHolder(holder, position)
is AdminMessageViewHolder -> bindAdminMessage(holder, position)
is AdminMessageViewHolder -> holder.bind((items[position] as DashboardItem.AdminMessages).adminMessage)
is AdsViewHolder -> bindAdsViewHolder(holder, position)
}
}
@ -733,39 +734,6 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<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) {
val item = (items[position] as DashboardItem.Ads).adBanner ?: return
val binding = adsViewHolder.binding
@ -819,9 +787,6 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
val adapter by lazy { DashboardConferencesAdapter() }
}
class AdminMessageViewHolder(val binding: ItemDashboardAdminMessageBinding) :
RecyclerView.ViewHolder(binding.root)
class AdsViewHolder(val binding: ItemDashboardAdsBinding) :
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) {
ONE_SEMESTER -> getGradeSubjects(
student = student,
semester = semesters.single { it.semesterId == semesterId },
semester = semesters.first { it.semesterId == semesterId },
forceRefresh = forceRefresh,
params = params,
)

View File

@ -22,6 +22,8 @@ import io.github.wulkanowy.databinding.ItemGradeStatisticsHeaderBinding
import io.github.wulkanowy.databinding.ItemGradeStatisticsPieBinding
import io.github.wulkanowy.utils.getThemeAttrColor
import javax.inject.Inject
import kotlin.math.max
import kotlin.math.roundToInt
class GradeStatisticsAdapter @Inject constructor() :
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
@ -269,7 +271,7 @@ class GradeStatisticsAdapter @Inject constructor() :
valueTextSize = 12f
valueTextColor = binding.root.context.getThemeAttrColor(android.R.attr.textColorPrimary)
valueFormatter = object : ValueFormatter() {
override fun getBarLabel(barEntry: BarEntry) = "${barEntry.y}%"
override fun getBarLabel(barEntry: BarEntry) = "${barEntry.y}"
}
colors = gradePointsColors
}
@ -304,15 +306,20 @@ class GradeStatisticsAdapter @Inject constructor() :
}
xAxis.setDrawLabels(false)
xAxis.setDrawGridLines(false)
val yMaxFromValues = (max(points.others, points.student)).roundToInt() + 30f
val yMaxFromValuesWithMargin = ((yMaxFromValues / 10.0).roundToInt() * 10).toFloat()
val yMax = yMaxFromValuesWithMargin.coerceAtLeast(100f)
val yLabelCount = (yMax / 10).toInt() + 1
with(axisLeft) {
axisMinimum = 0f
axisMaximum = 100f
labelCount = 11
axisMaximum = yMax
labelCount = yLabelCount
}
with(axisRight) {
axisMinimum = 0f
axisMaximum = 100f
labelCount = 11
axisMaximum = yMax
labelCount = yLabelCount
}
invalidate()
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -10,8 +10,11 @@ import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.databinding.FragmentLoginStudentSelectBinding
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.auth.AuthDialog
import io.github.wulkanowy.ui.modules.login.LoginActivity
import io.github.wulkanowy.ui.modules.login.LoginData
import io.github.wulkanowy.ui.modules.login.support.LoginSupportDialog
import io.github.wulkanowy.ui.modules.login.support.LoginSupportInfo
import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.openEmailClient
import io.github.wulkanowy.utils.openInternetBrowser
@ -106,21 +109,8 @@ class LoginStudentSelectFragment :
context?.openInternetBrowser("https://discord.gg/vccAQBr", ::showMessage)
}
override fun openEmail(lastError: String) {
context?.openEmailClient(
chooserTitle = requireContext().getString(R.string.login_email_intent_title),
email = "wulkanowyinc@gmail.com",
subject = requireContext().getString(R.string.login_email_subject),
body = requireContext().getString(
R.string.login_email_text,
"${appInfo.systemManufacturer} ${appInfo.systemModel}",
appInfo.systemVersion.toString(),
"${appInfo.versionName}-${appInfo.buildFlavor}",
"Select users to log in",
preferencesRepository.installationId,
lastError
)
)
override fun openEmail(supportInfo: LoginSupportInfo) {
LoginSupportDialog.newInstance(supportInfo).show(childFragmentManager, "support_dialog")
}
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.RegisterUnit
import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.data.repositories.SchoolsRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.resourceFlow
import io.github.wulkanowy.sdk.scrapper.login.AccountPermissionException
import io.github.wulkanowy.sdk.scrapper.login.InvalidSymbolException
import io.github.wulkanowy.services.sync.SyncManager
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.modules.login.LoginData
import io.github.wulkanowy.ui.modules.login.LoginErrorHandler
import io.github.wulkanowy.ui.modules.login.support.LoginSupportInfo
import io.github.wulkanowy.utils.AnalyticsHelper
import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.ifNullOrBlank
import io.github.wulkanowy.utils.isCurrent
import kotlinx.coroutines.flow.onEach
import timber.log.Timber
import javax.inject.Inject
class LoginStudentSelectPresenter @Inject constructor(
studentRepository: StudentRepository,
private val schoolsRepository: SchoolsRepository,
private val loginErrorHandler: LoginErrorHandler,
private val syncManager: SyncManager,
private val analytics: AnalyticsHelper,
@ -71,7 +73,14 @@ class LoginStudentSelectPresenter @Inject constructor(
students = it.dataOrNull.orEmpty()
when (it) {
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 -> {
errorHandler.dispatch(it.error)
lastError = it.error
@ -81,6 +90,23 @@ class LoginStudentSelectPresenter @Inject constructor(
}.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 { it.isEnabled }
.filter { student ->
student.student.semesters.any { semester ->
semester.isCurrent()
}
}
}
private fun createItems(): List<LoginStudentSelectItem> = buildList {
val notEmptySymbols = registerUser.symbols.filter { it.schools.isNotEmpty() }
val emptySymbols = registerUser.symbols.filter { it.schools.isEmpty() }
@ -236,16 +262,20 @@ class LoginStudentSelectPresenter @Inject constructor(
}
private fun registerStudents(students: List<LoginStudentSelectItem>) {
val studentsWithSemesters = students
.filterIsInstance<LoginStudentSelectItem.Student>().map { item ->
item.student.mapToStudentWithSemesters(
user = registerUser,
symbol = item.symbol,
unit = item.unit,
colors = appInfo.defaultColorsForAvatar,
)
}
resourceFlow { studentRepository.saveStudents(studentsWithSemesters) }
val filteredStudents = students.filterIsInstance<LoginStudentSelectItem.Student>()
val studentsWithSemesters = filteredStudents.map { item ->
item.student.mapToStudentWithSemesters(
user = registerUser,
symbol = item.symbol,
scrapperDomainSuffix = loginData.domainSuffix,
unit = item.unit,
colors = appInfo.defaultColorsForAvatar,
)
}
resourceFlow {
studentRepository.saveStudents(studentsWithSemesters)
schoolsRepository.logSchoolLogin(loginData, studentsWithSemesters)
}
.logResourceStatus("registration")
.onEach {
when (it) {
@ -253,11 +283,13 @@ class LoginStudentSelectPresenter @Inject constructor(
showProgress(true)
showContent(false)
}
is Resource.Success -> {
syncManager.startOneTimeSyncWorker(quiet = true)
view?.navigateToNext()
logRegisterEvent(studentsWithSemesters)
}
is Resource.Error -> {
view?.apply {
showProgress(false)
@ -280,28 +312,14 @@ class LoginStudentSelectPresenter @Inject constructor(
}
private fun onEmailClick() {
view?.openEmail(lastError?.message.ifNullOrBlank {
loginData.baseUrl + "/" + loginData.symbol + "\n" + registerUser.symbols.filterNot {
(it.error is AccountPermissionException || it.error is InvalidSymbolException) && it.symbol != loginData.symbol
}.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: " + registerUser.symbols.filter {
it.error is AccountPermissionException || it.error is InvalidSymbolException
}.joinToString(", ") { it.symbol }
})
view?.openEmail(
LoginSupportInfo(
loginData = loginData,
registerUser = registerUser,
lastErrorMessage = lastError?.message,
enteredSymbol = loginData.symbol,
)
)
}
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.modules.login.LoginData
import io.github.wulkanowy.ui.modules.login.support.LoginSupportInfo
interface LoginStudentSelectView : BaseView {
@ -23,5 +24,5 @@ interface LoginStudentSelectView : BaseView {
fun openDiscordInvite()
fun openEmail(lastError: String)
fun openEmail(supportInfo: LoginSupportInfo)
}

View File

@ -0,0 +1,149 @@
package io.github.wulkanowy.ui.modules.login.support
import android.os.Bundle
import android.util.Patterns
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.core.widget.doOnTextChanged
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.databinding.DialogLoginSupportBinding
import io.github.wulkanowy.sdk.scrapper.login.AccountPermissionException
import io.github.wulkanowy.sdk.scrapper.login.InvalidSymbolException
import io.github.wulkanowy.ui.base.BaseDialogFragment
import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.openEmailClient
import io.github.wulkanowy.utils.serializable
import javax.inject.Inject
@AndroidEntryPoint
class LoginSupportDialog : BaseDialogFragment<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 { onSubmitClick() }
}
}
private fun onSubmitClick() {
when {
binding.dialogLoginSupportSchoolInput.text.isNullOrBlank() -> {
with(binding.dialogLoginSupportSchoolLayout) {
isErrorEnabled = true
error = getString(R.string.error_field_required)
}
}
Patterns.EMAIL_ADDRESS.matcher(
binding.dialogLoginSupportSchoolInput.text.toString()
).matches() -> {
with(binding.dialogLoginSupportSchoolLayout) {
isErrorEnabled = true
error = getString(R.string.login_support_school_invalid)
}
}
else -> {
openEmailClientWithFilledTemplate()
dismiss()
}
}
}
private fun openEmailClientWithFilledTemplate() {
with(binding) {
context?.openEmailClient(
chooserTitle = requireContext().getString(R.string.login_email_intent_title),
email = "wulkanowyinc@gmail.com",
subject = requireContext().getString(R.string.login_email_subject),
body = requireContext().getString(
R.string.login_email_text,
"${appInfo.systemManufacturer} ${appInfo.systemModel}",
appInfo.systemVersion.toString(),
"${appInfo.versionName}-${appInfo.buildFlavor}",
supportInfo.loginData.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.modules.login.LoginActivity
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
@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() {
setErrorSymbol(getString(R.string.error_field_required))
}
@ -163,20 +176,7 @@ class LoginSymbolFragment :
)
}
override fun openEmail(host: String, lastError: String) {
context?.openEmailClient(
chooserTitle = requireContext().getString(R.string.login_email_intent_title),
email = "wulkanowyinc@gmail.com",
subject = requireContext().getString(R.string.login_email_subject),
body = requireContext().getString(
R.string.login_email_text,
"${appInfo.systemManufacturer} ${appInfo.systemModel}",
appInfo.systemVersion.toString(),
"${appInfo.versionName}-${appInfo.buildFlavor}",
"$host/${binding.loginSymbolName.text}",
preferencesRepository.installationId,
lastError
)
)
override fun openSupportDialog(supportInfo: LoginSupportInfo) {
LoginSupportDialog.newInstance(supportInfo).show(childFragmentManager, "support_dialog")
}
}

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.modules.login.LoginData
import io.github.wulkanowy.ui.modules.login.LoginErrorHandler
import io.github.wulkanowy.ui.modules.login.support.LoginSupportInfo
import io.github.wulkanowy.utils.AnalyticsHelper
import io.github.wulkanowy.utils.ifNullOrBlank
import kotlinx.coroutines.flow.onEach
@ -53,6 +54,10 @@ class LoginSymbolPresenter @Inject constructor(
view?.setErrorSymbolRequire()
return
}
if (isFormDefinitelyInvalid()) {
view?.setErrorSymbolDefinitelyInvalid()
return
}
loginData = loginData.copy(
symbol = view?.symbolValue?.getNormalizedSymbol(),
@ -62,6 +67,7 @@ class LoginSymbolPresenter @Inject constructor(
email = loginData.login,
password = loginData.password,
scrapperBaseUrl = loginData.baseUrl,
domainSuffix = loginData.domainSuffix,
symbol = loginData.symbol.orEmpty(),
)
}.onEach { user ->
@ -73,6 +79,7 @@ class LoginSymbolPresenter @Inject constructor(
showProgress(true)
showContent(false)
}
is Resource.Success -> {
when (user.data.symbols.size) {
0 -> {
@ -82,6 +89,7 @@ class LoginSymbolPresenter @Inject constructor(
showContact(true)
}
}
else -> {
val enteredSymbolDetails = user.data.symbols
.firstOrNull()
@ -106,6 +114,7 @@ class LoginSymbolPresenter @Inject constructor(
"error" to "No error"
)
}
is Resource.Error -> {
Timber.i("Login with symbol result: An exception occurred")
analytics.logEvent(
@ -129,17 +138,25 @@ class LoginSymbolPresenter @Inject constructor(
}.launch("login")
}
private fun isFormDefinitelyInvalid(): Boolean {
val definitelyInvalidSymbols = listOf("vulcan", "uonet", "wulkanowy", "standardowa")
val normalizedSymbol = view?.symbolValue.orEmpty().getNormalizedSymbol()
return normalizedSymbol in definitelyInvalidSymbols
}
fun onFaqClick() {
view?.openFaqPage()
}
fun onEmailClick() {
view?.openEmail(loginData.baseUrl, lastError?.message.ifNullOrBlank {
registerUser?.symbols?.flatMap { symbol ->
symbol.schools.map { it.error?.message } + symbol.error?.message
}?.filterNotNull()?.distinct()?.joinToString(";") {
it.take(46) + "..."
} ?: "blank"
})
view?.openSupportDialog(
LoginSupportInfo(
loginData = loginData,
registerUser = registerUser,
lastErrorMessage = lastError?.message,
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.ui.base.BaseView
import io.github.wulkanowy.ui.modules.login.LoginData
import io.github.wulkanowy.ui.modules.login.support.LoginSupportInfo
interface LoginSymbolView : BaseView {
@ -18,6 +19,8 @@ interface LoginSymbolView : BaseView {
fun setErrorSymbolInvalid()
fun setErrorSymbolDefinitelyInvalid()
fun setErrorSymbolRequire()
fun setErrorSymbol(message: String)
@ -40,5 +43,5 @@ interface LoginSymbolView : BaseView {
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
@Deprecated("Deprecated in Java")
@Suppress("DEPRECATION")
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
updateHelper.onActivityResult(requestCode, resultCode)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -58,7 +58,10 @@ class TeacherPresenter @Inject constructor(
.logResourceStatus("load teachers data")
.onResourceData {
view?.run {
updateData(it.filter { item -> item.name.isNotBlank() })
updateData(it
.filter { item -> item.name.isNotBlank() }
.sortedBy { it.name }
)
showContent(it.isNotEmpty())
showEmpty(it.isEmpty())
showErrorView(false)

View File

@ -35,7 +35,7 @@ class AdvancedFragment : PreferenceFragmentCompat(),
setPreferencesFromResource(R.xml.scheme_preferences_advanced, rootKey)
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
presenter.onSharedPreferenceChanged(key)
}

View File

@ -18,7 +18,8 @@ class AdvancedPresenter @Inject constructor(
Timber.i("Settings advanced view was initialized")
}
fun onSharedPreferenceChanged(key: String) {
fun onSharedPreferenceChanged(key: String?) {
key ?: return
Timber.i("Change settings $key")
analytics.logEvent("setting_changed", "name" to key)
}

View File

@ -39,7 +39,7 @@ class AppearanceFragment : PreferenceFragmentCompat(),
setPreferencesFromResource(R.xml.scheme_preferences_appearance, rootKey)
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
presenter.onSharedPreferenceChanged(key)
}

View File

@ -22,7 +22,8 @@ class AppearancePresenter @Inject constructor(
Timber.i("Settings appearance view was initialized")
}
fun onSharedPreferenceChanged(key: String) {
fun onSharedPreferenceChanged(key: String?) {
key ?: return
Timber.i("Change settings $key")
preferencesRepository.apply {

View File

@ -114,7 +114,7 @@ class NotificationsFragment : PreferenceFragmentCompat(),
setPreferencesFromResource(R.xml.scheme_preferences_notifications, rootKey)
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
presenter.onSharedPreferenceChanged(key)
}

View File

@ -38,7 +38,8 @@ class NotificationsPresenter @Inject constructor(
Timber.i("Settings notifications view was initialized")
}
fun onSharedPreferenceChanged(key: String) {
fun onSharedPreferenceChanged(key: String?) {
key ?: return
Timber.i("Change settings $key")
preferencesRepository.apply {

View File

@ -52,7 +52,7 @@ class SyncFragment : PreferenceFragmentCompat(),
setPreferencesFromResource(R.xml.scheme_preferences_sync, rootKey)
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
presenter.onSharedPreferenceChanged(key)
}

View File

@ -31,7 +31,8 @@ class SyncPresenter @Inject constructor(
setSyncDateInView()
}
fun onSharedPreferenceChanged(key: String) {
fun onSharedPreferenceChanged(key: String?) {
key ?: return
Timber.i("Change settings $key")
preferencesRepository.apply {
@ -52,10 +53,12 @@ class SyncPresenter @Inject constructor(
Timber.i("Setting sync now started")
analytics.logEvent("sync_now", "status" to "started")
}
WorkInfo.State.SUCCEEDED -> {
showMessage(syncSuccessString)
analytics.logEvent("sync_now", "status" to "success")
}
WorkInfo.State.FAILED -> {
showError(
syncFailedString,
@ -66,6 +69,7 @@ class SyncPresenter @Inject constructor(
)
analytics.logEvent("sync_now", "status" to "failed")
}
else -> Timber.d("Sync now state: ${workInfo?.state}")
}
if (workInfo?.state?.isFinished == true) {

View File

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

View File

@ -9,6 +9,7 @@ import android.view.View.GONE
import android.view.View.VISIBLE
import androidx.core.os.bundleOf
import androidx.core.text.parseAsHtml
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
@ -160,6 +161,10 @@ class TimetableFragment : BaseFragment<FragmentTimetableBinding>(R.layout.fragme
binding.timetableRecycler.visibility = if (show) VISIBLE else GONE
}
override fun showNavigation(show: Boolean) {
binding.timetableNavContainer.isVisible = true
}
override fun showPreButton(show: Boolean) {
binding.timetablePreviousButton.visibility = if (show) VISIBLE else View.INVISIBLE
}
@ -193,7 +198,9 @@ class TimetableFragment : BaseFragment<FragmentTimetableBinding>(R.layout.fragme
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putLong(SAVED_DATE_KEY, presenter.currentDate.toEpochDay())
presenter.currentDate?.toEpochDay()?.let {
outState.putLong(SAVED_DATE_KEY, it)
}
}
override fun onDestroyView() {

View File

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

View File

@ -1,23 +1,52 @@
package io.github.wulkanowy.ui.modules.timetable
import io.github.wulkanowy.data.*
import android.os.Handler
import android.os.Looper
import io.github.wulkanowy.data.dataOrNull
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.Timetable
import io.github.wulkanowy.data.enums.TimetableGapsMode.BETWEEN_AND_BEFORE_LESSONS
import io.github.wulkanowy.data.enums.TimetableGapsMode.NO_GAPS
import io.github.wulkanowy.data.enums.TimetableMode
import io.github.wulkanowy.data.flatResourceFlow
import io.github.wulkanowy.data.logResourceStatus
import io.github.wulkanowy.data.onResourceData
import io.github.wulkanowy.data.onResourceError
import io.github.wulkanowy.data.onResourceIntermediate
import io.github.wulkanowy.data.onResourceNotLoading
import io.github.wulkanowy.data.onResourceSuccess
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.SemesterRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.repositories.TimetableRepository
import io.github.wulkanowy.data.toFirstResult
import io.github.wulkanowy.data.waitForResult
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.utils.*
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.onEach
import io.github.wulkanowy.utils.AnalyticsHelper
import io.github.wulkanowy.utils.capitalise
import io.github.wulkanowy.utils.getLastSchoolDayIfHoliday
import io.github.wulkanowy.utils.isHolidays
import io.github.wulkanowy.utils.isJustFinished
import io.github.wulkanowy.utils.isShowTimeUntil
import io.github.wulkanowy.utils.left
import io.github.wulkanowy.utils.monday
import io.github.wulkanowy.utils.nextOrSameSchoolDay
import io.github.wulkanowy.utils.nextSchoolDay
import io.github.wulkanowy.utils.previousSchoolDay
import io.github.wulkanowy.utils.sunday
import io.github.wulkanowy.utils.toFormattedString
import io.github.wulkanowy.utils.until
import kotlinx.coroutines.flow.firstOrNull
import timber.log.Timber
import java.time.DayOfWeek
import java.time.Instant
import java.time.LocalDate
import java.time.LocalDate.*
import java.util.*
import java.time.LocalDate.now
import java.time.LocalDate.of
import java.time.LocalDate.ofEpochDay
import java.util.Timer
import javax.inject.Inject
import kotlin.concurrent.timer
@ -30,9 +59,10 @@ class TimetablePresenter @Inject constructor(
private val analytics: AnalyticsHelper,
) : BasePresenter<TimetableView>(errorHandler, studentRepository) {
private var baseDate: LocalDate = now().nextOrSameSchoolDay
private var initialDate: LocalDate? = null
private var isWeekendHasLessons: Boolean = false
lateinit var currentDate: LocalDate
var currentDate: LocalDate? = null
private set
private lateinit var lastError: Throwable
@ -44,23 +74,30 @@ class TimetablePresenter @Inject constructor(
view.initView()
Timber.i("Timetable was initialized")
errorHandler.showErrorMessage = ::showErrorViewOnError
reloadView(ofEpochDay(date ?: baseDate.toEpochDay()))
currentDate = date?.let(::ofEpochDay)
loadData()
if (currentDate.isHolidays) setBaseDateOnHolidays()
}
fun onPreviousDay() {
reloadView(currentDate.previousSchoolDay)
val date = if (isWeekendHasLessons) {
currentDate?.minusDays(1)
} else currentDate?.previousSchoolDay
reloadView(date ?: return)
loadData()
}
fun onNextDay() {
reloadView(currentDate.nextSchoolDay)
val date = if (isWeekendHasLessons) {
currentDate?.plusDays(1)
} else currentDate?.nextSchoolDay
reloadView(date ?: return)
loadData()
}
fun onPickDate() {
view?.showDatePickerDialog(currentDate)
view?.showDatePickerDialog(currentDate ?: return)
}
fun onDateSet(year: Int, month: Int, day: Int) {
@ -89,10 +126,8 @@ class TimetablePresenter @Inject constructor(
Timber.i("Timetable view is reselected")
view?.let { view ->
if (view.currentStackSize == 1) {
baseDate = now().nextOrSameSchoolDay
if (currentDate != baseDate) {
reloadView(baseDate)
if (currentDate != initialDate) {
reloadView(initialDate ?: return)
loadData()
} else if (!view.isViewEmpty) {
view.resetView()
@ -113,34 +148,25 @@ class TimetablePresenter @Inject constructor(
return true
}
private fun setBaseDateOnHolidays() {
flow {
val student = studentRepository.getCurrentStudent()
emit(semesterRepository.getCurrentSemester(student))
}.catch {
Timber.i("Loading semester result: An exception occurred")
}.onEach {
baseDate = baseDate.getLastSchoolDayIfHoliday(it.schoolYear)
currentDate = baseDate
reloadNavigation()
}.launch("holidays")
}
private fun loadData(forceRefresh: Boolean = false) {
flatResourceFlow {
val student = studentRepository.getCurrentStudent()
val semester = semesterRepository.getCurrentSemester(student)
checkInitialAndCurrentDate(student, semester)
timetableRepository.getTimetable(
student = student,
semester = semester,
start = currentDate,
end = currentDate,
start = currentDate ?: now(),
end = currentDate ?: now(),
forceRefresh = forceRefresh,
timetableType = TimetableRepository.TimetableType.NORMAL
)
}
.logResourceStatus("load timetable data")
.onResourceData {
isWeekendHasLessons = isWeekendHasLessons || isWeekendHasLessons(it.lessons)
view?.run {
enableSwipe(true)
showProgress(false)
@ -148,7 +174,8 @@ class TimetablePresenter @Inject constructor(
showContent(it.lessons.isNotEmpty())
showEmpty(it.lessons.isEmpty())
updateData(it.lessons)
setDayHeaderMessage(it.headers.singleOrNull { header -> header.date == currentDate }?.content)
setDayHeaderMessage(it.headers.find { header -> header.date == currentDate }?.content)
reloadNavigation()
}
}
.onResourceIntermediate { view?.showRefresh(true) }
@ -170,14 +197,54 @@ class TimetablePresenter @Inject constructor(
.launch()
}
private suspend fun checkInitialAndCurrentDate(student: Student, semester: Semester) {
if (initialDate == null) {
val lessons = timetableRepository.getTimetable(
student = student,
semester = semester,
start = now().monday,
end = now().sunday,
forceRefresh = false,
timetableType = TimetableRepository.TimetableType.NORMAL
).toFirstResult().dataOrNull?.lessons.orEmpty()
isWeekendHasLessons = isWeekendHasLessons(lessons)
initialDate = getInitialDate(semester)
}
if (currentDate == null) {
currentDate = initialDate
}
}
private fun isWeekendHasLessons(
lessons: List<Timetable>,
): Boolean = lessons.any {
it.date.dayOfWeek in listOf(
DayOfWeek.SATURDAY,
DayOfWeek.SUNDAY,
)
}
private fun getInitialDate(semester: Semester): LocalDate {
val now = now()
return when {
now.isHolidays -> now.getLastSchoolDayIfHoliday(semester.schoolYear)
isWeekendHasLessons -> now
else -> now.nextOrSameSchoolDay
}
}
private fun updateData(lessons: List<Timetable>) {
tickTimer?.cancel()
if (!prefRepository.showTimetableTimers) {
if (currentDate != now()) {
view?.updateData(createItems(lessons))
} else {
tickTimer = timer(period = 2_000) {
view?.updateData(createItems(lessons))
Handler(Looper.getMainLooper()).post {
view?.updateData(createItems(lessons))
}
}
}
}
@ -192,16 +259,38 @@ class TimetablePresenter @Inject constructor(
compareBy({ item -> item.number }, { item -> !item.isStudentPlan })
)
return filteredItems.mapIndexed { i, it ->
if (it.isStudentPlan) TimetableItem.Normal(
lesson = it,
showGroupsInPlan = prefRepository.showGroupsInPlan,
timeLeft = filteredItems.getTimeLeftForLesson(it, i),
onClick = ::onTimetableItemSelected
) else TimetableItem.Small(
lesson = it,
onClick = ::onTimetableItemSelected
)
var prevNum = when (prefRepository.showTimetableGaps) {
BETWEEN_AND_BEFORE_LESSONS -> 0
else -> null
}
return buildList {
filteredItems.forEachIndexed { i, it ->
if (prefRepository.showTimetableGaps != NO_GAPS && prevNum != null && it.number > prevNum!! + 1) {
val emptyLesson = TimetableItem.Empty(
numFrom = prevNum!! + 1,
numTo = it.number - 1
)
add(emptyLesson)
}
if (it.isStudentPlan) {
val normalLesson = TimetableItem.Normal(
lesson = it,
showGroupsInPlan = prefRepository.showGroupsInPlan,
timeLeft = filteredItems.getTimeLeftForLesson(it, i),
onClick = ::onTimetableItemSelected
)
add(normalLesson)
} else {
val smallLesson = TimetableItem.Small(
lesson = it,
onClick = ::onTimetableItemSelected
)
add(smallLesson)
}
prevNum = it.number
}
}
}
@ -242,7 +331,7 @@ class TimetablePresenter @Inject constructor(
private fun reloadView(date: LocalDate) {
currentDate = date
Timber.i("Reload timetable view with the date ${currentDate.toFormattedString()}")
Timber.i("Reload timetable view with the date ${currentDate?.toFormattedString()}")
view?.apply {
showProgress(true)
enableSwipe(false)
@ -255,10 +344,13 @@ class TimetablePresenter @Inject constructor(
}
private fun reloadNavigation() {
val currentDate = currentDate ?: return
view?.apply {
showPreButton(!currentDate.minusDays(1).isHolidays)
showNextButton(!currentDate.plusDays(1).isHolidays)
updateNavigationDay(currentDate.toFormattedString("EEEE, dd.MM").capitalise())
showNavigation(true)
}
}

View File

@ -36,6 +36,8 @@ interface TimetableView : BaseView {
fun showContent(show: Boolean)
fun showNavigation(show: Boolean)
fun showPreButton(show: Boolean)
fun showNextButton(show: Boolean)

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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,9 +1,7 @@
Wersja 2.0.5
Wersja 2.2.3
zaktualizowaliśmy wygląd aplikacji na (częściowo) zgodny z wytycznymi Material 3
dodaliśmy możliwość zmiany kolejności pozycji w menu dolnym
— poprawiliśmy sposób wyświetlania błędu o nieprawidłowym haśle na ekranie logowania
— od teraz zmiana ustawień liczenia średniej automatycznie odświeży listę ocen
— dodaliśmy okienko na wpisanie numeru PESEL, gdy dziennik wymaga dodatkowej autoryzacji dostępu
ułatwiliśmy przełączenie dnia na weekend w planie lekcji przy użyciu strzałek
poprawiliśmy wsparcie dla statystyk ocen z systemem punktowym
— poprawiliśmy sortowanie nauczycieli w widoku Szkoła i nauczyciele
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"
android:shape="rectangle">
<solid android:color="#FF000000" />
<corners android:radius="12dp" />
<corners android:radius="14dp" />
</shape>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#FFFFFBFF" />
<corners android:radius="12dp" />
<corners android:radius="14dp" />
</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"
android:width="24dp"
android:height="24dp"
android:width="20dp"
android:height="20dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path

View File

@ -1,6 +1,6 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:width="20dp"
android:height="20dp"
android:viewportWidth="24"
android:viewportHeight="24">
<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_height="match_parent"
android:background="@drawable/background_widget_timetable"
android:backgroundTint="?attr/colorSurface"
android:backgroundTint="?attr/colorSecondaryContainer"
android:clipToOutline="true"
android:orientation="vertical"
android:paddingHorizontal="16dp"
android:paddingHorizontal="12dp"
android:theme="@style/Wulkanowy.Widget.Theme"
tools:context=".ui.modules.timetablewidget.TimetableWidgetProvider">
@ -16,23 +16,47 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingVertical="16dp">
android:paddingTop="12dp"
android:paddingBottom="8dp">
<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
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginHorizontal="12dp"
android:layout_weight="1"
android:lines="1"
android:text="Pon, 03.10"
android:textAppearance="?attr/textAppearanceHeadline5"
android:text="Pon, 19.05"
android:textSize="18sp"
tools:ignore="HardcodedText" />
<ImageButton
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginStart="12dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/all_prev"
android:rotation="180"
@ -51,46 +75,273 @@
app:tint="?attr/colorPrimary"
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
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:showDividers="middle">
android:paddingBottom="12dp">
<ImageView
<LinearLayout
android:layout_width="match_parent"
android:layout_height="56dp"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:importantForAccessibility="no"
android:src="@drawable/background_widget_item_timetable"
app:tint="?attr/backgroundColor" />
android:background="@drawable/background_widget_item_timetable"
android:backgroundTint="?attr/colorSurface"
android:gravity="center_vertical"
android:minHeight="48dp"
android:orientation="horizontal"
android:paddingHorizontal="12dp"
android:paddingVertical="8dp">
<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_height="56dp"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:importantForAccessibility="no"
android:src="@drawable/background_widget_item_timetable"
app:tint="?attr/backgroundColor" />
android:background="@drawable/background_widget_item_timetable"
android:backgroundTint="?attr/colorSurface"
android:gravity="center_vertical"
android:minHeight="48dp"
android:orientation="horizontal"
android:paddingHorizontal="12dp"
android:paddingVertical="8dp">
<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_height="56dp"
android:importantForAccessibility="no"
android:src="@drawable/background_widget_item_timetable"
app:tint="?attr/backgroundColor" />
android:layout_height="wrap_content"
android:background="@drawable/background_widget_item_timetable"
android:backgroundTint="?attr/colorSurface"
android:gravity="center_vertical"
android:minHeight="48dp"
android:orientation="horizontal"
android:paddingHorizontal="12dp"
android:paddingVertical="8dp">
<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>

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