1
0

Compare commits

..

42 Commits
1.8.3 ... 1.9.2

Author SHA1 Message Date
c4672b8de9 Merge branch 'hotfix/1.9.2' 2023-03-08 21:28:57 +01:00
1b40e339b7 Version 1.9.2 2023-03-08 21:28:48 +01:00
ee5ac46493 Show invalid symbol message when nonexistent symbol entered (#2143) 2023-03-08 09:11:25 +01:00
ef398f7409 Add missing override to RemoteConfigHelper.initialize() 2023-03-07 22:29:37 +01:00
5331bf90cd Use user agent template from firebase remote config (#2139)
* Use user agent template from firebase remote config

* Improve base class usage, activation refactor
2023-03-07 18:10:20 +01:00
a495fcbc5f Fix saving attachements with same url but from different messages (#2137) 2023-03-02 18:01:48 +01:00
f11354dd35 Fix marking message as read (#2102) 2023-03-01 22:59:44 +01:00
4bb1198735 Merge branch 'release/1.9.1' 2023-01-05 23:01:43 +01:00
3eb74da945 Version 1.9.1 2023-01-05 23:01:38 +01:00
5161fdd543 Add missing info to student selection email reports (#2096) 2023-01-05 21:47:53 +00:00
377e0c3a0d Fix school name text wrap in dashboard and student details (#2095) 2023-01-05 22:42:35 +01:00
1c9860091a Bump robolectric from 4.9.1 to 4.9.2 (#2093) 2023-01-03 07:45:25 +00:00
a383f7409d Merge branch 'release/1.9.0' into develop 2023-01-01 21:57:53 +01:00
9a8fb593c0 Merge branch 'release/1.9.0' 2023-01-01 21:57:47 +01:00
f4c6e0ad1b Version 1.9.0 2023-01-01 21:57:39 +01:00
b30b7c3318 New Crowdin updates (#2068) 2023-01-01 21:52:46 +01:00
897eac050a Refactor student selection screen (#2087) 2023-01-01 20:26:32 +01:00
83974b6550 Fix NPE when trying to remove a message from mailbox that doesn't match any student (#2090) 2023-01-01 20:21:28 +01:00
7efd106658 Update date in LICENSE file (#2089) 2023-01-01 12:16:09 +01:00
9cedab979c Bump robolectric from 4.9 to 4.9.1 (#2088) 2022-12-26 20:07:25 +00:00
510e2d5b88 Fix html entities parsing in school announcements (#2086) 2022-12-25 04:40:58 +01:00
63d6a0b325 Merge branch 'hotfix/1.8.3' into develop 2022-12-21 13:30:26 +01:00
ede5914d70 Automatically show current student mailbox only when there is only one mailbox available (#2085)
* Automatically show current student mailbox only when there is only one mailbox for this student available

* Fallback to 'unknown' mailbox key if there is no matching mailbox to message
2022-12-21 00:31:29 +01:00
09c968f273 Merge branch 'hotfix/1.8.2' into develop 2022-12-21 00:15:02 +01:00
f1479d489b Bump play-services-ads from 21.3.0 to 21.4.0 (#2083) 2022-12-20 12:28:26 +00:00
fba4e85311 Bump firebase-bom from 31.1.0 to 31.1.1 (#2079) 2022-12-14 21:52:41 +00:00
4a5991ade4 Bump hianalytics from 6.9.0.300 to 6.9.0.301 (#2080) 2022-12-14 21:43:04 +00:00
a735c378f1 Bump fragment-ktx from 1.5.4 to 1.5.5 (#2077) 2022-12-14 21:42:21 +00:00
217ebfc549 Fix app name in french (#2072) 2022-12-14 22:41:57 +01:00
df5155f1c7 Fix a typo in excuse message subject (#2071) 2022-12-05 15:45:07 +01:00
b93c0222a2 Fix conference details strings (#2070) 2022-12-05 15:44:30 +01:00
083ca34f1b Bump hianalytics from 6.8.0.300 to 6.9.0.300 (#2069) 2022-12-05 13:32:46 +00:00
5d5dfd4eb4 Bump mockk from 1.13.2 to 1.13.3 (#2067) 2022-12-01 18:38:59 +00:00
429fdfa4a0 Update project to Android SDK 33 (#2011) 2022-12-01 19:02:25 +01:00
302d723cfb Suppress menu deprecations (#2031) 2022-12-01 18:14:28 +01:00
8f50ee82b3 Change fakelog to https (#2063) 2022-11-28 19:50:14 +01:00
9dc1220496 Bump about_libraries from 10.5.1 to 10.5.2 (#2066) 2022-11-28 18:36:14 +00:00
ae39bd94e5 Bump hilt_version from 2.44.1 to 2.44.2 (#2058) 2022-11-22 20:42:38 +00:00
85ce23845f Bump firebase-bom from 31.0.3 to 31.1.0 (#2057) 2022-11-21 20:51:23 +00:00
890d60811b Bump agcp from 1.7.3.301 to 1.7.3.302 (#2059) 2022-11-21 20:51:07 +00:00
49e68f5c8b Bump agconnect-crash from 1.7.3.300 to 1.7.3.302 (#2060) 2022-11-21 20:50:47 +00:00
1df4679db8 Merge branch 'release/1.8.1' into develop 2022-11-20 00:05:47 +01:00
119 changed files with 5070 additions and 602 deletions

View File

@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier same "printed page" as the copyright notice for easier
identification within third-party archives. identification within third-party archives.
Copyright 2022 Wulkanowy Copyright 2023 Wulkanowy
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.

View File

@ -16,15 +16,15 @@ apply from: 'hooks.gradle'
android { android {
namespace 'io.github.wulkanowy' namespace 'io.github.wulkanowy'
compileSdkVersion 32 compileSdkVersion 33
defaultConfig { defaultConfig {
applicationId "io.github.wulkanowy" applicationId "io.github.wulkanowy"
testApplicationId "io.github.tests.wulkanowy" testApplicationId "io.github.tests.wulkanowy"
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 32 targetSdkVersion 33
versionCode 118 versionCode 121
versionName "1.8.3" versionName "1.9.2"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resValue "string", "app_name", "Wulkanowy" resValue "string", "app_name", "Wulkanowy"
@ -161,8 +161,8 @@ play {
defaultToAppBundles = false defaultToAppBundles = false
track = 'production' track = 'production'
releaseStatus = com.github.triplet.gradle.androidpublisher.ReleaseStatus.IN_PROGRESS releaseStatus = com.github.triplet.gradle.androidpublisher.ReleaseStatus.IN_PROGRESS
userFraction = 0.10d userFraction = 0.50d
updatePriority = 5 updatePriority = 2
enabled.set(false) enabled.set(false)
} }
@ -181,23 +181,23 @@ ext {
android_hilt = "1.0.0" android_hilt = "1.0.0"
room = "2.4.3" room = "2.4.3"
chucker = "3.5.2" chucker = "3.5.2"
mockk = "1.13.2" mockk = "1.13.3"
coroutines = "1.6.4" coroutines = "1.6.4"
} }
dependencies { dependencies {
implementation "io.github.wulkanowy:sdk:1.8.3" implementation "io.github.wulkanowy:sdk:1.9.2"
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.8' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.8'
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1" implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines"
implementation "androidx.core:core-ktx:1.8.0" implementation "androidx.core:core-ktx:1.9.0"
implementation 'androidx.core:core-splashscreen:1.0.0' implementation 'androidx.core:core-splashscreen:1.0.0'
implementation "androidx.activity:activity-ktx:1.5.1" implementation "androidx.activity:activity-ktx:1.6.1"
implementation "androidx.appcompat:appcompat:1.5.1" implementation "androidx.appcompat:appcompat:1.5.1"
implementation "androidx.fragment:fragment-ktx:1.5.4" implementation "androidx.fragment:fragment-ktx:1.5.5"
implementation "androidx.annotation:annotation:1.5.0" implementation "androidx.annotation:annotation:1.5.0"
implementation "androidx.preference:preference-ktx:1.2.0" implementation "androidx.preference:preference-ktx:1.2.0"
@ -237,20 +237,22 @@ dependencies {
implementation 'com.github.bastienpaulfr:Treessence:1.0.5' implementation 'com.github.bastienpaulfr:Treessence:1.0.5'
implementation "com.mikepenz:aboutlibraries-core:$about_libraries" implementation "com.mikepenz:aboutlibraries-core:$about_libraries"
implementation "io.coil-kt:coil:2.2.2" implementation "io.coil-kt:coil:2.2.2"
implementation "io.github.wulkanowy:AppKillerManager:3.0.0" implementation "io.github.wulkanowy:AppKillerManager:3.0.1"
implementation 'me.xdrop:fuzzywuzzy:1.4.0' implementation 'me.xdrop:fuzzywuzzy:1.4.0'
implementation 'com.fredporciuncula:flow-preferences:1.8.0' implementation 'com.fredporciuncula:flow-preferences:1.8.0'
implementation 'org.apache.commons:commons-text:1.10.0'
playImplementation platform('com.google.firebase:firebase-bom:31.0.3') playImplementation platform('com.google.firebase:firebase-bom:31.1.1')
playImplementation 'com.google.firebase:firebase-analytics-ktx' playImplementation 'com.google.firebase:firebase-analytics-ktx'
playImplementation 'com.google.firebase:firebase-messaging:' playImplementation 'com.google.firebase:firebase-messaging:'
playImplementation 'com.google.firebase:firebase-crashlytics:' playImplementation 'com.google.firebase:firebase-crashlytics:'
playImplementation 'com.google.firebase:firebase-config-ktx'
playImplementation 'com.google.android.play:core:1.10.3' playImplementation 'com.google.android.play:core:1.10.3'
playImplementation 'com.google.android.play:core-ktx:1.8.1' playImplementation 'com.google.android.play:core-ktx:1.8.1'
playImplementation 'com.google.android.gms:play-services-ads:21.3.0' playImplementation 'com.google.android.gms:play-services-ads:21.4.0'
hmsImplementation 'com.huawei.hms:hianalytics:6.8.0.300' hmsImplementation 'com.huawei.hms:hianalytics:6.9.0.301'
hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.7.3.300' hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.7.3.302'
releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker" releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker"
@ -263,7 +265,7 @@ dependencies {
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines" testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines"
testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version" testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
testImplementation 'org.robolectric:robolectric:4.9' testImplementation 'org.robolectric:robolectric:4.9.2'
testImplementation "androidx.test:runner:1.5.1" testImplementation "androidx.test:runner:1.5.1"
testImplementation "androidx.test.ext:junit:1.1.4" testImplementation "androidx.test.ext:junit:1.1.4"
testImplementation "androidx.test:core:1.5.0" testImplementation "androidx.test:core:1.5.0"
@ -271,9 +273,9 @@ dependencies {
testImplementation "com.google.dagger:hilt-android-testing:$hilt_version" testImplementation "com.google.dagger:hilt-android-testing:$hilt_version"
kaptTest "com.google.dagger:hilt-android-compiler:$hilt_version" kaptTest "com.google.dagger:hilt-android-compiler:$hilt_version"
androidTestImplementation "androidx.test:core:1.4.0" androidTestImplementation "androidx.test:core:1.5.0"
androidTestImplementation "androidx.test:runner:1.4.0" androidTestImplementation "androidx.test:runner:1.5.1"
androidTestImplementation "androidx.test.ext:junit:1.1.3" androidTestImplementation "androidx.test.ext:junit:1.1.4"
androidTestImplementation "io.mockk:mockk-android:$mockk" androidTestImplementation "io.mockk:mockk-android:$mockk"
androidTestImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version" androidTestImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
} }

File diff suppressed because it is too large Load Diff

View File

@ -2,4 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/colorPrimary" /> <background android:drawable="@color/colorPrimary" />
<foreground android:drawable="@drawable/ic_launcher_foreground_dev" /> <foreground android:drawable="@drawable/ic_launcher_foreground_dev" />
</adaptive-icon> <monochrome android:drawable="@drawable/ic_launcher_foreground_dev_mono" />
</adaptive-icon>

View File

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/colorPrimary" />
<foreground android:drawable="@drawable/ic_launcher_foreground_dev" />
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

View File

@ -0,0 +1,7 @@
package io.github.wulkanowy.utils
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class RemoteConfigHelper @Inject constructor() : BaseRemoteConfigHelper()

View File

@ -0,0 +1,7 @@
package io.github.wulkanowy.utils
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class RemoteConfigHelper @Inject constructor() : BaseRemoteConfigHelper()

View File

@ -8,7 +8,8 @@
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.VIBRATE" /> <uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/> <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<queries> <queries>
<intent> <intent>
@ -36,13 +37,14 @@
<application <application
android:name=".WulkanowyApp" android:name=".WulkanowyApp"
android:allowBackup="false" android:allowBackup="false"
android:dataExtractionRules="@xml/data_extraction_rules"
android:enableOnBackInvokedCallback="true"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config" android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="false" android:supportsRtl="false"
android:theme="@style/WulkanowyTheme" android:theme="@style/WulkanowyTheme"
tools:ignore="GoogleAppIndexingWarning,UnusedAttribute"> tools:ignore="DataExtractionRules,UnusedAttribute">
<activity <activity
android:name=".ui.modules.splash.SplashActivity" android:name=".ui.modules.splash.SplashActivity"
android:exported="true" android:exported="true"

View File

@ -34,11 +34,15 @@ class WulkanowyApp : Application(), Configuration.Provider {
@Inject @Inject
lateinit var adsHelper: AdsHelper lateinit var adsHelper: AdsHelper
@Inject
lateinit var remoteConfigHelper: RemoteConfigHelper
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
initializeAppLanguage() initializeAppLanguage()
themeManager.applyDefaultTheme() themeManager.applyDefaultTheme()
adsHelper.initialize() adsHelper.initialize()
remoteConfigHelper.initialize()
initLogging() initLogging()
} }

View File

@ -19,6 +19,7 @@ import io.github.wulkanowy.data.db.SharedPrefProvider
import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.RemoteConfigHelper
import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
@ -36,10 +37,11 @@ internal class DataModule {
@Singleton @Singleton
@Provides @Provides
fun provideSdk(chuckerInterceptor: ChuckerInterceptor) = fun provideSdk(chuckerInterceptor: ChuckerInterceptor, remoteConfig: RemoteConfigHelper) =
Sdk().apply { Sdk().apply {
androidVersion = android.os.Build.VERSION.RELEASE androidVersion = android.os.Build.VERSION.RELEASE
buildTag = android.os.Build.MODEL buildTag = android.os.Build.MODEL
userAgentTemplate = remoteConfig.userAgentTemplate
setSimpleHttpLogger { Timber.d(it) } setSimpleHttpLogger { Timber.d(it) }
// for debug only // for debug only

View File

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

View File

@ -2,16 +2,14 @@ package io.github.wulkanowy.data.db.entities
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey
import java.io.Serializable import java.io.Serializable
@Entity(tableName = "MessageAttachments") @Entity(
tableName = "MessageAttachments",
primaryKeys = ["message_global_key", "url", "filename"],
)
data class MessageAttachment( data class MessageAttachment(
@PrimaryKey
@ColumnInfo(name = "real_id")
val realId: Int,
@ColumnInfo(name = "message_global_key") @ColumnInfo(name = "message_global_key")
val messageGlobalKey: String, val messageGlobalKey: String,

View File

@ -0,0 +1,17 @@
package io.github.wulkanowy.data.db.migrations
import androidx.room.DeleteColumn
import androidx.room.migration.AutoMigrationSpec
import androidx.sqlite.db.SupportSQLiteDatabase
@DeleteColumn(
tableName = "MessageAttachments",
columnName = "real_id",
)
class Migration55 : AutoMigrationSpec {
override fun onPostMigrate(db: SupportSQLiteDatabase) {
db.execSQL("DELETE FROM Messages")
db.execSQL("DELETE FROM MessageAttachments")
}
}

View File

@ -40,7 +40,6 @@ fun List<SdkMessage>.mapToEntities(
fun List<SdkMessageAttachment>.mapToEntities(messageGlobalKey: String) = map { fun List<SdkMessageAttachment>.mapToEntities(messageGlobalKey: String) = map {
MessageAttachment( MessageAttachment(
messageGlobalKey = messageGlobalKey, messageGlobalKey = messageGlobalKey,
realId = it.url.hashCode(),
url = it.url, url = it.url,
filename = it.filename filename = it.filename
) )

View File

@ -0,0 +1,87 @@
package io.github.wulkanowy.data.mappers
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.data.pojos.*
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.sdk.mapper.mapSemesters
import java.time.Instant
import io.github.wulkanowy.sdk.scrapper.register.RegisterStudent as SdkRegisterStudent
import io.github.wulkanowy.sdk.scrapper.register.RegisterUser as SdkRegisterUser
fun SdkRegisterUser.mapToPojo(password: String) = RegisterUser(
email = email,
login = login,
password = password,
baseUrl = baseUrl,
loginType = loginType,
symbols = symbols.map { registerSymbol ->
RegisterSymbol(
symbol = registerSymbol.symbol,
error = registerSymbol.error,
userName = registerSymbol.userName,
schools = registerSymbol.schools.map {
RegisterUnit(
userLoginId = it.userLoginId,
schoolId = it.schoolId,
schoolName = it.schoolName,
schoolShortName = it.schoolShortName,
parentIds = it.parentIds,
studentIds = it.studentIds,
employeeIds = it.employeeIds,
error = it.error,
students = it.subjects
.filterIsInstance<SdkRegisterStudent>()
.map { registerSubject ->
RegisterStudent(
studentId = registerSubject.studentId,
studentName = registerSubject.studentName,
studentSecondName = registerSubject.studentSecondName,
studentSurname = registerSubject.studentSurname,
className = registerSubject.className,
classId = registerSubject.classId,
isParent = registerSubject.isParent,
semesters = registerSubject.semesters
.mapSemesters()
.mapToEntities(registerSubject.studentId),
)
},
)
}
)
}
)
fun RegisterStudent.mapToStudentWithSemesters(
user: RegisterUser,
symbol: RegisterSymbol,
unit: RegisterUnit,
colors: List<Long>,
): StudentWithSemesters = StudentWithSemesters(
semesters = semesters,
student = Student(
email = user.login, // for compatibility
userName = symbol.userName,
userLoginId = unit.userLoginId,
isParent = isParent,
className = className,
classId = classId,
studentId = studentId,
symbol = symbol.symbol,
loginType = user.loginType.name,
schoolName = unit.schoolName,
schoolShortName = unit.schoolShortName,
schoolSymbol = unit.schoolId,
studentName = "$studentName $studentSurname",
loginMode = Sdk.Mode.SCRAPPER.name,
scrapperBaseUrl = user.baseUrl,
mobileBaseUrl = "",
certificateKey = "",
privateKey = "",
password = user.password,
isCurrent = false,
registrationDate = Instant.now(),
).apply {
avatarColor = colors.random()
},
)

View File

@ -0,0 +1,43 @@
package io.github.wulkanowy.data.pojos
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.sdk.scrapper.Scrapper
data class RegisterUser(
val email: String,
val password: String,
val login: String, // may be the same as email
val baseUrl: String,
val loginType: Scrapper.LoginType,
val symbols: List<RegisterSymbol>,
) : java.io.Serializable
data class RegisterSymbol(
val symbol: String,
val error: Throwable?,
val userName: String,
val schools: List<RegisterUnit>,
) : java.io.Serializable
data class RegisterUnit(
val userLoginId: Int,
val schoolId: String,
val schoolName: String,
val schoolShortName: String,
val parentIds: List<Int>,
val studentIds: List<Int>,
val employeeIds: List<Int>,
val error: Throwable?,
val students: List<RegisterStudent>,
) : java.io.Serializable
data class RegisterStudent(
val studentId: Int,
val studentName: String,
val studentSecondName: String,
val studentSurname: String,
val className: String,
val classId: Int,
val isParent: Boolean,
val semesters: List<Semester>,
) : java.io.Serializable

View File

@ -103,7 +103,10 @@ class MessageRepository @Inject constructor(
messagesDb.loadMessageWithAttachment(message.messageGlobalKey) messagesDb.loadMessageWithAttachment(message.messageGlobalKey)
}, },
fetch = { fetch = {
sdk.init(student).getMessageDetails(it!!.message.messageGlobalKey, markAsRead) sdk.init(student).getMessageDetails(
messageKey = it!!.message.messageGlobalKey,
markAsRead = message.unread && markAsRead,
)
}, },
saveFetchResult = { old, new -> saveFetchResult = { old, new ->
checkNotNull(old) { "Fetched message no longer exist!" } checkNotNull(old) { "Fetched message no longer exist!" }
@ -178,7 +181,7 @@ class MessageRepository @Inject constructor(
).first() ).first()
} }
suspend fun deleteMessage(student: Student, mailbox: Mailbox, message: Message) { suspend fun deleteMessage(student: Student, mailbox: Mailbox?, message: Message) {
deleteMessages(student, mailbox, listOf(message)) deleteMessages(student, mailbox, listOf(message))
} }

View File

@ -11,6 +11,8 @@ import io.github.wulkanowy.data.db.entities.StudentNickAndAvatar
import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.data.exceptions.NoCurrentStudentException import io.github.wulkanowy.data.exceptions.NoCurrentStudentException
import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.data.mappers.mapToPojo
import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.DispatchersProvider import io.github.wulkanowy.utils.DispatchersProvider
@ -52,6 +54,14 @@ class StudentRepository @Inject constructor(
sdk.getStudentsFromScrapper(email, password, scrapperBaseUrl, symbol) sdk.getStudentsFromScrapper(email, password, scrapperBaseUrl, symbol)
.mapToEntities(password, appInfo.defaultColorsForAvatar) .mapToEntities(password, appInfo.defaultColorsForAvatar)
suspend fun getUserSubjectsFromScrapper(
email: String,
password: String,
scrapperBaseUrl: String,
symbol: String
): RegisterUser = sdk.getUserSubjectsFromScrapper(email, password, scrapperBaseUrl, symbol)
.mapToPojo(password)
suspend fun getStudentsHybrid( suspend fun getStudentsHybrid(
email: String, email: String,
password: String, password: String,

View File

@ -37,7 +37,7 @@ class ErrorDialog : DialogFragment() {
} }
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val error = requireArguments().getSerializable(ARGUMENT_KEY) as Throwable val error = requireArguments().serializable<Throwable>(ARGUMENT_KEY)
val binding = DialogErrorBinding.inflate(layoutInflater) val binding = DialogErrorBinding.inflate(layoutInflater)
binding.bindErrorDetails(error) binding.bindErrorDetails(error)

View File

@ -1,6 +1,9 @@
package io.github.wulkanowy.ui.base package io.github.wulkanowy.ui.base
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.content.pm.PackageManager.GET_ACTIVITIES import android.content.pm.PackageManager.GET_ACTIVITIES
import android.os.Build
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
@ -41,9 +44,8 @@ class ThemeManager @Inject constructor(private val preferencesRepository: Prefer
) )
} }
private fun isThemeApplicable(activity: AppCompatActivity) = private fun isThemeApplicable(activity: AppCompatActivity): Boolean =
activity.packageManager getPackageInfo(activity)
.getPackageInfo(activity.packageName, GET_ACTIVITIES)
.activities .activities
.singleOrNull { it.name == activity::class.java.canonicalName } .singleOrNull { it.name == activity::class.java.canonicalName }
?.theme ?.theme
@ -52,4 +54,14 @@ class ThemeManager @Inject constructor(private val preferencesRepository: Prefer
|| it == R.style.WulkanowyTheme_Login || it == R.style.WulkanowyTheme_Login_Black || it == R.style.WulkanowyTheme_Login || it == R.style.WulkanowyTheme_Login_Black
|| it == R.style.WulkanowyTheme_MessageSend || it == R.style.WulkanowyTheme_MessageSend_Black || it == R.style.WulkanowyTheme_MessageSend || it == R.style.WulkanowyTheme_MessageSend_Black
} }
@Suppress("DEPRECATION")
private fun getPackageInfo(activity: AppCompatActivity): PackageInfo {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
activity.packageManager.getPackageInfo(
activity.packageName,
PackageManager.PackageInfoFlags.of(GET_ACTIVITIES.toLong())
)
} else activity.packageManager.getPackageInfo(activity.packageName, GET_ACTIVITIES)
}
} }

View File

@ -34,6 +34,7 @@ class AccountFragment : BaseFragment<FragmentAccountBinding>(R.layout.fragment_a
override val titleStringId = R.string.account_title override val titleStringId = R.string.account_title
@Suppress("DEPRECATION")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setHasOptionsMenu(true) setHasOptionsMenu(true)

View File

@ -6,6 +6,7 @@ import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.os.bundleOf
import androidx.core.view.get import androidx.core.view.get
import androidx.core.view.isVisible import androidx.core.view.isVisible
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
@ -21,6 +22,7 @@ import io.github.wulkanowy.ui.modules.studentinfo.StudentInfoFragment
import io.github.wulkanowy.ui.modules.studentinfo.StudentInfoView import io.github.wulkanowy.ui.modules.studentinfo.StudentInfoView
import io.github.wulkanowy.utils.createNameInitialsDrawable import io.github.wulkanowy.utils.createNameInitialsDrawable
import io.github.wulkanowy.utils.nickOrName import io.github.wulkanowy.utils.nickOrName
import io.github.wulkanowy.utils.serializable
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
@ -37,12 +39,12 @@ class AccountDetailsFragment :
private const val ARGUMENT_KEY = "Data" private const val ARGUMENT_KEY = "Data"
fun newInstance(student: Student) = fun newInstance(student: Student) = AccountDetailsFragment().apply {
AccountDetailsFragment().apply { arguments = bundleOf(ARGUMENT_KEY to student)
arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, student) } }
}
} }
@Suppress("DEPRECATION")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setHasOptionsMenu(true) setHasOptionsMenu(true)
@ -51,7 +53,7 @@ class AccountDetailsFragment :
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
binding = FragmentAccountDetailsBinding.bind(view) binding = FragmentAccountDetailsBinding.bind(view)
presenter.onAttachView(this, requireArguments()[ARGUMENT_KEY] as Student) presenter.onAttachView(this, requireArguments().serializable(ARGUMENT_KEY))
} }
override fun initView() { override fun initView() {

View File

@ -4,11 +4,13 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.databinding.DialogAccountEditBinding import io.github.wulkanowy.databinding.DialogAccountEditBinding
import io.github.wulkanowy.ui.base.BaseDialogFragment import io.github.wulkanowy.ui.base.BaseDialogFragment
import io.github.wulkanowy.utils.serializable
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
@ -24,12 +26,9 @@ class AccountEditDialog : BaseDialogFragment<DialogAccountEditBinding>(), Accoun
private const val ARGUMENT_KEY = "student_with_semesters" private const val ARGUMENT_KEY = "student_with_semesters"
fun newInstance(student: Student) = fun newInstance(student: Student) = AccountEditDialog().apply {
AccountEditDialog().apply { arguments = bundleOf(ARGUMENT_KEY to student)
arguments = Bundle().apply { }
putSerializable(ARGUMENT_KEY, student)
}
}
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -45,7 +44,7 @@ class AccountEditDialog : BaseDialogFragment<DialogAccountEditBinding>(), Accoun
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
presenter.onAttachView(this, requireArguments()[ARGUMENT_KEY] as Student) presenter.onAttachView(this, requireArguments().serializable(ARGUMENT_KEY))
} }
override fun initView() { override fun initView() {

View File

@ -4,6 +4,7 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.data.db.entities.StudentWithSemesters
@ -13,6 +14,7 @@ import io.github.wulkanowy.ui.modules.account.AccountAdapter
import io.github.wulkanowy.ui.modules.account.AccountFragment import io.github.wulkanowy.ui.modules.account.AccountFragment
import io.github.wulkanowy.ui.modules.account.AccountItem import io.github.wulkanowy.ui.modules.account.AccountItem
import io.github.wulkanowy.ui.modules.main.MainActivity import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.utils.serializable
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
@ -30,9 +32,7 @@ class AccountQuickDialog : BaseDialogFragment<DialogAccountQuickBinding>(), Acco
fun newInstance(studentsWithSemesters: List<StudentWithSemesters>) = fun newInstance(studentsWithSemesters: List<StudentWithSemesters>) =
AccountQuickDialog().apply { AccountQuickDialog().apply {
arguments = Bundle().apply { arguments = bundleOf(STUDENTS_ARGUMENT_KEY to studentsWithSemesters.toTypedArray())
putSerializable(STUDENTS_ARGUMENT_KEY, studentsWithSemesters.toTypedArray())
}
} }
} }
@ -49,8 +49,8 @@ class AccountQuickDialog : BaseDialogFragment<DialogAccountQuickBinding>(), Acco
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val studentsWithSemesters = val studentsWithSemesters = requireArguments()
(requireArguments()[STUDENTS_ARGUMENT_KEY] as Array<StudentWithSemesters>).toList() .serializable<Array<StudentWithSemesters>>(STUDENTS_ARGUMENT_KEY).toList()
presenter.onAttachView(this, studentsWithSemesters) presenter.onAttachView(this, studentsWithSemesters)
} }

View File

@ -4,11 +4,13 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import io.github.wulkanowy.data.db.entities.Attendance import io.github.wulkanowy.data.db.entities.Attendance
import io.github.wulkanowy.databinding.DialogAttendanceBinding import io.github.wulkanowy.databinding.DialogAttendanceBinding
import io.github.wulkanowy.utils.descriptionRes import io.github.wulkanowy.utils.descriptionRes
import io.github.wulkanowy.utils.lifecycleAwareVariable import io.github.wulkanowy.utils.lifecycleAwareVariable
import io.github.wulkanowy.utils.serializable
import io.github.wulkanowy.utils.toFormattedString import io.github.wulkanowy.utils.toFormattedString
class AttendanceDialog : DialogFragment() { class AttendanceDialog : DialogFragment() {
@ -22,16 +24,14 @@ class AttendanceDialog : DialogFragment() {
private const val ARGUMENT_KEY = "Item" private const val ARGUMENT_KEY = "Item"
fun newInstance(exam: Attendance) = AttendanceDialog().apply { fun newInstance(exam: Attendance) = AttendanceDialog().apply {
arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, exam) } arguments = bundleOf(ARGUMENT_KEY to exam)
} }
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setStyle(STYLE_NO_TITLE, 0) setStyle(STYLE_NO_TITLE, 0)
arguments?.run { attendance = requireArguments().serializable(ARGUMENT_KEY)
attendance = getSerializable(ARGUMENT_KEY) as Attendance
}
} }
override fun onCreateView( override fun onCreateView(

View File

@ -84,6 +84,7 @@ class AttendanceFragment : BaseFragment<FragmentAttendanceBinding>(R.layout.frag
} }
} }
@Suppress("DEPRECATION")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setHasOptionsMenu(true) setHasOptionsMenu(true)

View File

@ -4,11 +4,13 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import io.github.wulkanowy.data.db.entities.Conference import io.github.wulkanowy.data.db.entities.Conference
import io.github.wulkanowy.databinding.DialogConferenceBinding import io.github.wulkanowy.databinding.DialogConferenceBinding
import io.github.wulkanowy.utils.lifecycleAwareVariable import io.github.wulkanowy.utils.lifecycleAwareVariable
import io.github.wulkanowy.utils.serializable
import io.github.wulkanowy.utils.toFormattedString import io.github.wulkanowy.utils.toFormattedString
class ConferenceDialog : DialogFragment() { class ConferenceDialog : DialogFragment() {
@ -22,16 +24,14 @@ class ConferenceDialog : DialogFragment() {
private const val ARGUMENT_KEY = "item" private const val ARGUMENT_KEY = "item"
fun newInstance(conference: Conference) = ConferenceDialog().apply { fun newInstance(conference: Conference) = ConferenceDialog().apply {
arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, conference) } arguments = bundleOf(ARGUMENT_KEY to conference)
} }
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setStyle(STYLE_NO_TITLE, 0) setStyle(STYLE_NO_TITLE, 0)
arguments?.let { conference = requireArguments().serializable(ARGUMENT_KEY)
conference = it.getSerializable(ARGUMENT_KEY) as Conference
}
} }
override fun onCreateView( override fun onCreateView(
@ -57,4 +57,4 @@ class ConferenceDialog : DialogFragment() {
conferenceDialogAgendaTitle.isVisible = conference.agenda.isNotBlank() conferenceDialogAgendaTitle.isVisible = conference.agenda.isNotBlank()
} }
} }
} }

View File

@ -61,6 +61,7 @@ class DashboardFragment : BaseFragment<FragmentDashboardBinding>(R.layout.fragme
fun newInstance() = DashboardFragment() fun newInstance() = DashboardFragment()
} }
@Suppress("DEPRECATION")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setHasOptionsMenu(true) setHasOptionsMenu(true)

View File

@ -1,9 +1,7 @@
package io.github.wulkanowy.ui.modules.debug.logviewer package io.github.wulkanowy.ui.modules.debug.logviewer
import android.content.Intent import android.content.Intent
import android.content.Intent.EXTRA_EMAIL import android.content.Intent.*
import android.content.Intent.EXTRA_STREAM
import android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION
import android.os.Bundle import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
@ -36,6 +34,7 @@ class LogViewerFragment : BaseFragment<FragmentLogviewerBinding>(R.layout.fragme
fun newInstance() = LogViewerFragment() fun newInstance() = LogViewerFragment()
} }
@Suppress("DEPRECATION")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setHasOptionsMenu(true) setHasOptionsMenu(true)

View File

@ -4,12 +4,14 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Exam import io.github.wulkanowy.data.db.entities.Exam
import io.github.wulkanowy.databinding.DialogExamBinding import io.github.wulkanowy.databinding.DialogExamBinding
import io.github.wulkanowy.utils.lifecycleAwareVariable import io.github.wulkanowy.utils.lifecycleAwareVariable
import io.github.wulkanowy.utils.openCalendarEventAdd import io.github.wulkanowy.utils.openCalendarEventAdd
import io.github.wulkanowy.utils.serializable
import io.github.wulkanowy.utils.toFormattedString import io.github.wulkanowy.utils.toFormattedString
import java.time.LocalTime import java.time.LocalTime
@ -24,16 +26,14 @@ class ExamDialog : DialogFragment() {
private const val ARGUMENT_KEY = "Item" private const val ARGUMENT_KEY = "Item"
fun newInstance(exam: Exam) = ExamDialog().apply { fun newInstance(exam: Exam) = ExamDialog().apply {
arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, exam) } arguments = bundleOf(ARGUMENT_KEY to exam)
} }
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setStyle(STYLE_NO_TITLE, 0) setStyle(STYLE_NO_TITLE, 0)
arguments?.run { exam = requireArguments().serializable(ARGUMENT_KEY)
exam = getSerializable(ARGUMENT_KEY) as Exam
}
} }
override fun onCreateView( override fun onCreateView(

View File

@ -51,6 +51,7 @@ class GradeFragment : BaseFragment<FragmentGradeBinding>(R.layout.fragment_grade
override val currentPageIndex get() = binding.gradeViewPager.currentItem override val currentPageIndex get() = binding.gradeViewPager.currentItem
@Suppress("DEPRECATION")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setHasOptionsMenu(true) setHasOptionsMenu(true)

View File

@ -5,6 +5,7 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.View.GONE import android.view.View.GONE
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Grade import io.github.wulkanowy.data.db.entities.Grade
@ -27,22 +28,19 @@ class GradeDetailsDialog : DialogFragment() {
private const val COLOR_THEME_KEY = "Theme" private const val COLOR_THEME_KEY = "Theme"
fun newInstance(grade: Grade, colorTheme: GradeColorTheme) = fun newInstance(grade: Grade, colorTheme: GradeColorTheme) = GradeDetailsDialog().apply {
GradeDetailsDialog().apply { arguments = bundleOf(
arguments = Bundle().apply { ARGUMENT_KEY to grade,
putSerializable(ARGUMENT_KEY, grade) COLOR_THEME_KEY to colorTheme
putSerializable(COLOR_THEME_KEY, colorTheme) )
} }
}
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setStyle(STYLE_NO_TITLE, 0) setStyle(STYLE_NO_TITLE, 0)
arguments?.run { grade = requireArguments().serializable(ARGUMENT_KEY)
grade = getSerializable(ARGUMENT_KEY) as Grade gradeColorTheme = requireArguments().serializable(COLOR_THEME_KEY)
gradeColorTheme = getSerializable(COLOR_THEME_KEY) as GradeColorTheme
}
} }
override fun onCreateView( override fun onCreateView(

View File

@ -5,9 +5,7 @@ import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.View.GONE import android.view.View.*
import android.view.View.INVISIBLE
import android.view.View.VISIBLE
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R import io.github.wulkanowy.R
@ -42,6 +40,7 @@ class GradeDetailsFragment :
override val isViewEmpty override val isViewEmpty
get() = gradeDetailsAdapter.itemCount == 0 get() = gradeDetailsAdapter.itemCount == 0
@Suppress("DEPRECATION")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setHasOptionsMenu(true) setHasOptionsMenu(true)

View File

@ -15,6 +15,7 @@ import io.github.wulkanowy.ui.modules.grade.GradeFragment
import io.github.wulkanowy.ui.modules.grade.GradeView import io.github.wulkanowy.ui.modules.grade.GradeView
import io.github.wulkanowy.utils.dpToPx import io.github.wulkanowy.utils.dpToPx
import io.github.wulkanowy.utils.getThemeAttrColor import io.github.wulkanowy.utils.getThemeAttrColor
import io.github.wulkanowy.utils.serializable
import io.github.wulkanowy.utils.setOnItemSelectedListener import io.github.wulkanowy.utils.setOnItemSelectedListener
import javax.inject.Inject import javax.inject.Inject
@ -48,8 +49,8 @@ class GradeStatisticsFragment :
messageContainer = binding.gradeStatisticsRecycler messageContainer = binding.gradeStatisticsRecycler
presenter.onAttachView( presenter.onAttachView(
view = this, view = this,
type = savedInstanceState?.getSerializable(SAVED_CHART_TYPE) as? GradeStatisticsItem.DataType, type = savedInstanceState?.serializable(SAVED_CHART_TYPE),
subjectName = savedInstanceState?.getSerializable(SAVED_SUBJECT_NAME) as? String, subjectName = savedInstanceState?.serializable(SAVED_SUBJECT_NAME),
) )
} }

View File

@ -7,6 +7,7 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import androidx.core.os.bundleOf
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R import io.github.wulkanowy.R
@ -14,6 +15,7 @@ import io.github.wulkanowy.data.db.entities.Homework
import io.github.wulkanowy.databinding.DialogHomeworkBinding import io.github.wulkanowy.databinding.DialogHomeworkBinding
import io.github.wulkanowy.ui.base.BaseDialogFragment import io.github.wulkanowy.ui.base.BaseDialogFragment
import io.github.wulkanowy.utils.openInternetBrowser import io.github.wulkanowy.utils.openInternetBrowser
import io.github.wulkanowy.utils.serializable
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
@ -35,16 +37,14 @@ class HomeworkDetailsDialog : BaseDialogFragment<DialogHomeworkBinding>(), Homew
private const val ARGUMENT_KEY = "Item" private const val ARGUMENT_KEY = "Item"
fun newInstance(homework: Homework) = HomeworkDetailsDialog().apply { fun newInstance(homework: Homework) = HomeworkDetailsDialog().apply {
arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, homework) } arguments = bundleOf(ARGUMENT_KEY to homework)
} }
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setStyle(STYLE_NO_TITLE, 0) setStyle(STYLE_NO_TITLE, 0)
arguments?.run { homework = requireArguments().serializable(ARGUMENT_KEY)
homework = getSerializable(ARGUMENT_KEY) as Homework
}
} }
override fun onCreateView( override fun onCreateView(

View File

@ -2,13 +2,17 @@ package io.github.wulkanowy.ui.modules.login
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build.VERSION_CODES.TIRAMISU
import android.os.Bundle import android.os.Bundle
import android.view.MenuItem import android.view.MenuItem
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager.POP_BACK_STACK_INCLUSIVE
import androidx.fragment.app.commit import androidx.fragment.app.commit
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.databinding.ActivityLoginBinding import io.github.wulkanowy.databinding.ActivityLoginBinding
import io.github.wulkanowy.ui.base.BaseActivity import io.github.wulkanowy.ui.base.BaseActivity
import io.github.wulkanowy.ui.modules.login.advanced.LoginAdvancedFragment import io.github.wulkanowy.ui.modules.login.advanced.LoginAdvancedFragment
@ -16,6 +20,9 @@ import io.github.wulkanowy.ui.modules.login.form.LoginFormFragment
import io.github.wulkanowy.ui.modules.login.recover.LoginRecoverFragment import io.github.wulkanowy.ui.modules.login.recover.LoginRecoverFragment
import io.github.wulkanowy.ui.modules.login.studentselect.LoginStudentSelectFragment import io.github.wulkanowy.ui.modules.login.studentselect.LoginStudentSelectFragment
import io.github.wulkanowy.ui.modules.login.symbol.LoginSymbolFragment import io.github.wulkanowy.ui.modules.login.symbol.LoginSymbolFragment
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.notifications.NotificationsFragment
import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.UpdateHelper import io.github.wulkanowy.utils.UpdateHelper
import javax.inject.Inject import javax.inject.Inject
@ -28,6 +35,9 @@ class LoginActivity : BaseActivity<LoginPresenter, ActivityLoginBinding>(), Logi
@Inject @Inject
lateinit var updateHelper: UpdateHelper lateinit var updateHelper: UpdateHelper
@Inject
lateinit var appInfo: AppInfo
companion object { companion object {
fun getStartIntent(context: Context) = Intent(context, LoginActivity::class.java) fun getStartIntent(context: Context) = Intent(context, LoginActivity::class.java)
} }
@ -55,7 +65,7 @@ class LoginActivity : BaseActivity<LoginPresenter, ActivityLoginBinding>(), Logi
} }
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == android.R.id.home) onBackPressed() if (item.itemId == android.R.id.home) onBackPressedDispatcher.onBackPressed()
return true return true
} }
@ -67,8 +77,24 @@ class LoginActivity : BaseActivity<LoginPresenter, ActivityLoginBinding>(), Logi
openFragment(LoginSymbolFragment.newInstance(loginData)) openFragment(LoginSymbolFragment.newInstance(loginData))
} }
fun navigateToStudentSelect(studentsWithSemesters: List<StudentWithSemesters>) { fun navigateToStudentSelect(loginData: LoginData, registerUser: RegisterUser) {
openFragment(LoginStudentSelectFragment.newInstance(studentsWithSemesters)) openFragment(LoginStudentSelectFragment.newInstance(loginData, registerUser))
}
fun navigateToNotifications() {
val isNotificationsPermissionRequired = appInfo.systemVersion >= TIRAMISU
val isPermissionGranted = ContextCompat.checkSelfPermission(
this, "android.permission.POST_NOTIFICATIONS"
) == PackageManager.PERMISSION_GRANTED
if (isNotificationsPermissionRequired && !isPermissionGranted) {
openFragment(NotificationsFragment.newInstance(), clearBackStack = true)
} else navigateToFinish()
}
fun navigateToFinish() {
startActivity(MainActivity.getStartIntent(this))
finish()
} }
fun onAdvancedLoginClick() { fun onAdvancedLoginClick() {
@ -80,6 +106,8 @@ class LoginActivity : BaseActivity<LoginPresenter, ActivityLoginBinding>(), Logi
} }
private fun openFragment(fragment: Fragment, clearBackStack: Boolean = false) { private fun openFragment(fragment: Fragment, clearBackStack: Boolean = false) {
supportFragmentManager.popBackStack(fragment::class.java.name, POP_BACK_STACK_INCLUSIVE)
supportFragmentManager.commit { supportFragmentManager.commit {
replace(R.id.loginContainer, fragment) replace(R.id.loginContainer, fragment)
setReorderingAllowed(true) setReorderingAllowed(true)

View File

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

View File

@ -8,7 +8,7 @@ import android.widget.ArrayAdapter
import androidx.core.widget.doOnTextChanged import androidx.core.widget.doOnTextChanged
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.databinding.FragmentLoginAdvancedBinding import io.github.wulkanowy.databinding.FragmentLoginAdvancedBinding
import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.base.BaseFragment
@ -327,8 +327,8 @@ class LoginAdvancedFragment :
(activity as? LoginActivity)?.navigateToSymbolFragment(loginData) (activity as? LoginActivity)?.navigateToSymbolFragment(loginData)
} }
override fun navigateToStudentSelect(studentsWithSemesters: List<StudentWithSemesters>) { override fun navigateToStudentSelect(loginData: LoginData, registerUser: RegisterUser) {
(activity as? LoginActivity)?.navigateToStudentSelect(studentsWithSemesters) (activity as? LoginActivity)?.navigateToStudentSelect(loginData, registerUser)
} }
override fun onResume() { override fun onResume() {

View File

@ -4,9 +4,15 @@ import io.github.wulkanowy.data.Resource
import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.data.logResourceStatus import io.github.wulkanowy.data.logResourceStatus
import io.github.wulkanowy.data.onResourceNotLoading import io.github.wulkanowy.data.onResourceNotLoading
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.StudentRepository import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.resourceFlow import io.github.wulkanowy.data.resourceFlow
import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.sdk.scrapper.Scrapper
import io.github.wulkanowy.sdk.scrapper.getNormalizedSymbol
import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.modules.login.LoginData import io.github.wulkanowy.ui.modules.login.LoginData
import io.github.wulkanowy.ui.modules.login.LoginErrorHandler import io.github.wulkanowy.ui.modules.login.LoginErrorHandler
@ -142,19 +148,23 @@ class LoginAdvancedPresenter @Inject constructor(
is Resource.Success -> { is Resource.Success -> {
analytics.logEvent( analytics.logEvent(
"registration_form", "registration_form",
"success" to true, "success" to true,
"students" to it.data.size, "students" to it.data.size,
"error" to "No error" "error" to "No error"
) )
val loginData = LoginData( val loginData = LoginData(
login = view?.formUsernameValue.orEmpty().trim(), login = view?.formUsernameValue.orEmpty().trim(),
password = view?.formPassValue.orEmpty().trim(), password = view?.formPassValue.orEmpty().trim(),
baseUrl = view?.formHostValue.orEmpty().trim() baseUrl = view?.formHostValue.orEmpty().trim(),
) symbol = view?.formSymbolValue.orEmpty().trim().getNormalizedSymbol(),
when (it.data.size) { )
0 -> view?.navigateToSymbol(loginData) when (it.data.size) {
else -> view?.navigateToStudentSelect(it.data) 0 -> view?.navigateToSymbol(loginData)
} else -> view?.navigateToStudentSelect(
loginData = loginData,
registerUser = it.data.toRegisterUser(loginData),
)
}
} }
is Resource.Error -> { is Resource.Error -> {
analytics.logEvent( analytics.logEvent(
@ -173,6 +183,58 @@ class LoginAdvancedPresenter @Inject constructor(
}.launch("login") }.launch("login")
} }
private fun List<StudentWithSemesters>.toRegisterUser(loginData: LoginData) = RegisterUser(
email = loginData.login,
password = loginData.password,
login = loginData.login,
baseUrl = loginData.baseUrl,
loginType = firstOrNull()?.student?.loginType?.let(
Scrapper.LoginType::valueOf
) ?: Scrapper.LoginType.AUTO,
symbols = this
.groupBy { students -> students.student.symbol }
.map { (symbol, students) ->
RegisterSymbol(
symbol = symbol,
error = null,
userName = "",
schools = students
.groupBy { student ->
Triple(
first = student.student.schoolSymbol,
second = student.student.userLoginId,
third = student.student.schoolShortName
)
}
.map { (groupKey, students) ->
val (schoolId, loginId, schoolName) = groupKey
RegisterUnit(
students = students.map {
RegisterStudent(
studentId = it.student.studentId,
studentName = it.student.studentName,
studentSecondName = it.student.studentName,
studentSurname = it.student.studentName,
className = it.student.className,
classId = it.student.classId,
isParent = it.student.isParent,
semesters = it.semesters,
)
},
userLoginId = loginId,
schoolId = schoolId,
schoolName = schoolName,
schoolShortName = schoolName,
parentIds = listOf(),
studentIds = listOf(),
employeeIds = listOf(),
error = null
)
}
)
},
)
private suspend fun getStudentsAppropriatesToLoginType(): List<StudentWithSemesters> { private suspend fun getStudentsAppropriatesToLoginType(): List<StudentWithSemesters> {
val email = view?.formUsernameValue.orEmpty() val email = view?.formUsernameValue.orEmpty()
val password = view?.formPassValue.orEmpty() val password = view?.formPassValue.orEmpty()

View File

@ -1,6 +1,7 @@
package io.github.wulkanowy.ui.modules.login.advanced package io.github.wulkanowy.ui.modules.login.advanced
import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.ui.base.BaseView import io.github.wulkanowy.ui.base.BaseView
import io.github.wulkanowy.ui.modules.login.LoginData import io.github.wulkanowy.ui.modules.login.LoginData
@ -72,7 +73,7 @@ interface LoginAdvancedView : BaseView {
fun navigateToSymbol(loginData: LoginData) fun navigateToSymbol(loginData: LoginData)
fun navigateToStudentSelect(studentsWithSemesters: List<StudentWithSemesters>) fun navigateToStudentSelect(loginData: LoginData, registerUser: RegisterUser)
fun setErrorPinRequired() fun setErrorPinRequired()

View File

@ -9,7 +9,7 @@ import androidx.core.view.isVisible
import androidx.core.widget.doOnTextChanged import androidx.core.widget.doOnTextChanged
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.databinding.FragmentLoginFormBinding import io.github.wulkanowy.databinding.FragmentLoginFormBinding
import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.base.BaseFragment
@ -226,8 +226,8 @@ class LoginFormFragment : BaseFragment<FragmentLoginFormBinding>(R.layout.fragme
(activity as? LoginActivity)?.navigateToSymbolFragment(loginData) (activity as? LoginActivity)?.navigateToSymbolFragment(loginData)
} }
override fun navigateToStudentSelect(studentsWithSemesters: List<StudentWithSemesters>) { override fun navigateToStudentSelect(loginData: LoginData, registerUser: RegisterUser) {
(activity as? LoginActivity)?.navigateToStudentSelect(studentsWithSemesters) (activity as? LoginActivity)?.navigateToStudentSelect(loginData, registerUser)
} }
override fun openAdvancedLogin() { override fun openAdvancedLogin() {

View File

@ -93,7 +93,7 @@ class LoginFormPresenter @Inject constructor(
if (!validateCredentials(email, password, host)) return if (!validateCredentials(email, password, host)) return
resourceFlow { resourceFlow {
studentRepository.getStudentsScrapper( studentRepository.getUserSubjectsFromScrapper(
email = email, email = email,
password = password, password = password,
scrapperBaseUrl = host, scrapperBaseUrl = host,
@ -109,14 +109,14 @@ class LoginFormPresenter @Inject constructor(
} }
} }
.onResourceSuccess { .onResourceSuccess {
when (it.size) { val loginData = LoginData(email, password, host, symbol)
0 -> view?.navigateToSymbol(LoginData(email, password, host)) when (it.symbols.size) {
else -> view?.navigateToStudentSelect(it) 0 -> view?.navigateToSymbol(loginData)
else -> view?.navigateToStudentSelect(loginData, it)
} }
analytics.logEvent( analytics.logEvent(
"registration_form", "registration_form",
"success" to true, "success" to true,
"students" to it.size,
"scrapperBaseUrl" to host, "scrapperBaseUrl" to host,
"error" to "No error" "error" to "No error"
) )
@ -134,7 +134,6 @@ class LoginFormPresenter @Inject constructor(
analytics.logEvent( analytics.logEvent(
"registration_form", "registration_form",
"success" to false, "success" to false,
"students" to -1,
"scrapperBaseUrl" to host, "scrapperBaseUrl" to host,
"error" to it.message.ifNullOrBlank { "No message" } "error" to it.message.ifNullOrBlank { "No message" }
) )

View File

@ -1,6 +1,6 @@
package io.github.wulkanowy.ui.modules.login.form package io.github.wulkanowy.ui.modules.login.form
import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.ui.base.BaseView import io.github.wulkanowy.ui.base.BaseView
import io.github.wulkanowy.ui.modules.login.LoginData import io.github.wulkanowy.ui.modules.login.LoginData
@ -60,7 +60,7 @@ interface LoginFormView : BaseView {
fun navigateToSymbol(loginData: LoginData) fun navigateToSymbol(loginData: LoginData)
fun navigateToStudentSelect(studentsWithSemesters: List<StudentWithSemesters>) fun navigateToStudentSelect(loginData: LoginData, registerUser: RegisterUser)
fun openPrivacyPolicyPage() fun openPrivacyPolicyPage()

View File

@ -98,7 +98,7 @@ class LoginRecoverFragment :
loginRecoverButton.setOnClickListener { presenter.onRecoverClick() } loginRecoverButton.setOnClickListener { presenter.onRecoverClick() }
loginRecoverErrorRetry.setOnClickListener { presenter.onRecoverClick() } loginRecoverErrorRetry.setOnClickListener { presenter.onRecoverClick() }
loginRecoverErrorDetails.setOnClickListener { presenter.onDetailsClick() } loginRecoverErrorDetails.setOnClickListener { presenter.onDetailsClick() }
loginRecoverLogin.setOnClickListener { (activity as LoginActivity).onBackPressed() } loginRecoverLogin.setOnClickListener { (activity as LoginActivity).onBackPressedDispatcher.onBackPressed() }
} }
with(bindingLocal.loginRecoverHost) { with(bindingLocal.loginRecoverHost) {

View File

@ -2,65 +2,182 @@ package io.github.wulkanowy.ui.modules.login.studentselect
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.recyclerview.widget.DiffUtil.ItemCallback
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.R
import io.github.wulkanowy.databinding.ItemLoginStudentSelectBinding import io.github.wulkanowy.databinding.*
import javax.inject.Inject import javax.inject.Inject
@SuppressLint("SetTextI18n")
class LoginStudentSelectAdapter @Inject constructor() : class LoginStudentSelectAdapter @Inject constructor() :
RecyclerView.Adapter<LoginStudentSelectAdapter.ItemViewHolder>() { ListAdapter<LoginStudentSelectItem, RecyclerView.ViewHolder>(Differ) {
private val checkedList = mutableMapOf<Int, Boolean>() override fun getItemViewType(position: Int): Int = getItem(position).type.ordinal
var items = emptyList<Pair<StudentWithSemesters, Boolean>>() override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
set(value) { val inflater = LayoutInflater.from(parent.context)
field = value return when (LoginStudentSelectItemType.values()[viewType]) {
checkedList.clear() LoginStudentSelectItemType.EMPTY_SYMBOLS_HEADER -> EmptySymbolsHeaderViewHolder(
ItemLoginStudentSelectEmptySymbolHeaderBinding.inflate(inflater, parent, false),
)
LoginStudentSelectItemType.SYMBOL_HEADER -> SymbolsHeaderViewHolder(
ItemLoginStudentSelectHeaderSymbolBinding.inflate(inflater, parent, false)
)
LoginStudentSelectItemType.SCHOOL_HEADER -> SchoolHeaderViewHolder(
ItemLoginStudentSelectHeaderSchoolBinding.inflate(inflater, parent, false)
)
LoginStudentSelectItemType.STUDENT -> StudentViewHolder(
ItemLoginStudentSelectStudentBinding.inflate(inflater, parent, false)
)
LoginStudentSelectItemType.HELP -> HelpViewHolder(
ItemLoginStudentSelectHelpBinding.inflate(inflater, parent, false)
)
} }
}
var onClickListener: (StudentWithSemesters, alreadySaved: Boolean) -> Unit = { _, _ -> } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is EmptySymbolsHeaderViewHolder -> holder.bind(getItem(position) as LoginStudentSelectItem.EmptySymbolsHeader)
is SymbolsHeaderViewHolder -> holder.bind(getItem(position) as LoginStudentSelectItem.SymbolHeader)
is SchoolHeaderViewHolder -> holder.bind(getItem(position) as LoginStudentSelectItem.SchoolHeader)
is StudentViewHolder -> holder.bind(getItem(position) as LoginStudentSelectItem.Student)
is HelpViewHolder -> holder.bind(getItem(position) as LoginStudentSelectItem.Help)
}
}
override fun getItemCount() = items.size private class EmptySymbolsHeaderViewHolder(
private val binding: ItemLoginStudentSelectEmptySymbolHeaderBinding,
) : RecyclerView.ViewHolder(binding.root) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ItemViewHolder( fun bind(item: LoginStudentSelectItem.EmptySymbolsHeader) {
ItemLoginStudentSelectBinding.inflate(LayoutInflater.from(parent.context), parent, false) with(binding) {
) loginStudentSelectEmptySymbolChevron.rotation = if (item.isExpanded) 270f else 90f
root.setOnClickListener { item.onClick() }
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
val (studentAndSemesters, alreadySaved) = items[position]
val student = studentAndSemesters.student
val semesters = studentAndSemesters.semesters
val diary = semesters.maxByOrNull { it.semesterId }
with(holder.binding) {
loginItemName.text = "${student.studentName} ${diary?.diaryName.orEmpty()}"
loginItemSchool.text = student.schoolName
loginItemName.isEnabled = !alreadySaved
loginItemSchool.isEnabled = !alreadySaved
loginItemSignedIn.visibility = if (alreadySaved) View.VISIBLE else View.GONE
with(loginItemCheck) {
isEnabled = !alreadySaved
keyListener = null
isChecked = checkedList[position] ?: false
} }
}
}
root.setOnClickListener { private class SymbolsHeaderViewHolder(
onClickListener(studentAndSemesters, alreadySaved) private val binding: ItemLoginStudentSelectHeaderSymbolBinding,
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: LoginStudentSelectItem.SymbolHeader) {
with(binding) {
loginStudentSelectHeaderSymbolValue.text = buildString {
append(root.context.getString(R.string.mobile_device_symbol))
append(": ")
append(item.humanReadableName ?: item.symbol.symbol)
if (!item.humanReadableName.isNullOrBlank()) {
append(" (${item.symbol.symbol})")
}
}
loginStudentSelectHeaderSymbolUsername.text = item.symbol.userName
loginStudentSelectHeaderSymbolUsername.isVisible = item.symbol.userName.isNotBlank()
loginStudentSelectHeaderSymbolError.text = item.symbol.error?.message
loginStudentSelectHeaderSymbolError.isVisible = item.symbol.error != null
loginStudentSelectHeaderSymbolError.maxLines = when {
item.isErrorExpanded -> Int.MAX_VALUE
else -> 2
}
if (item.symbol.error != null) {
root.setOnClickListener { item.onClick(item.symbol) }
} else root.setOnClickListener(null)
}
}
}
private class SchoolHeaderViewHolder(
private val binding: ItemLoginStudentSelectHeaderSchoolBinding,
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: LoginStudentSelectItem.SchoolHeader) {
with(binding) {
loginStudentSelectHeaderSchoolName.text = buildString {
append(item.unit.schoolName.trim())
append(" (")
append(item.unit.schoolShortName)
append(")")
}
loginStudentSelectHeaderSchoolDetails.isVisible = item.unit.students.isEmpty()
loginStudentSelectHeaderSchoolError.text = item.unit.error?.message
loginStudentSelectHeaderSchoolError.isVisible = item.unit.error != null
loginStudentSelectHeaderSchoolError.maxLines = when {
item.isErrorExpanded -> Int.MAX_VALUE
else -> 2
}
if (item.unit.error != null) {
root.setOnClickListener { item.onClick(item.unit) }
} else root.setOnClickListener(null)
}
}
}
private class StudentViewHolder(
private val binding: ItemLoginStudentSelectStudentBinding,
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: LoginStudentSelectItem.Student) {
val student = item.student
val semesters = student.semesters
val diary = semesters.maxByOrNull { it.semesterId }
with(binding) {
loginItemName.text = "${student.studentName} ${student.studentSurname}"
loginItemName.isEnabled = item.isEnabled
loginItemSignedIn.text = if (!item.isEnabled) {
root.context.getString(R.string.login_signed_in)
} else diary?.diaryName
with(loginItemCheck) { with(loginItemCheck) {
if (isEnabled) { keyListener = null
isChecked = !isChecked isEnabled = item.isEnabled
checkedList[position] = isChecked isChecked = item.isSelected || !item.isEnabled
} }
root.isEnabled = item.isEnabled
root.setOnClickListener {
item.onClick(item)
} }
} }
} }
} }
class ItemViewHolder(val binding: ItemLoginStudentSelectBinding) : private class HelpViewHolder(
RecyclerView.ViewHolder(binding.root) private val binding: ItemLoginStudentSelectHelpBinding,
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: LoginStudentSelectItem.Help) {
with(binding) {
loginStudentSelectHelpSymbol.isVisible = item.isSymbolButtonVisible
loginStudentSelectHelpSymbol.setOnClickListener { item.onEnterSymbolClick() }
loginStudentSelectHelpMail.setOnClickListener { item.onContactUsClick() }
loginStudentSelectHelpDiscord.setOnClickListener { item.onDiscordClick() }
}
}
}
private object Differ : ItemCallback<LoginStudentSelectItem>() {
override fun areItemsTheSame(
oldItem: LoginStudentSelectItem, newItem: LoginStudentSelectItem
): Boolean = when {
oldItem is LoginStudentSelectItem.EmptySymbolsHeader && newItem is LoginStudentSelectItem.EmptySymbolsHeader -> true
oldItem is LoginStudentSelectItem.SymbolHeader && newItem is LoginStudentSelectItem.SymbolHeader -> {
oldItem.symbol == newItem.symbol
}
oldItem is LoginStudentSelectItem.Student && newItem is LoginStudentSelectItem.Student -> {
oldItem.student == newItem.student
}
else -> oldItem == newItem
}
override fun areContentsTheSame(
oldItem: LoginStudentSelectItem, newItem: LoginStudentSelectItem
): Boolean = oldItem == newItem
}
} }

View File

@ -2,21 +2,20 @@ package io.github.wulkanowy.ui.modules.login.studentselect
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.recyclerview.widget.LinearLayoutManager import androidx.core.view.isVisible
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.databinding.FragmentLoginStudentSelectBinding import io.github.wulkanowy.databinding.FragmentLoginStudentSelectBinding
import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.login.LoginActivity import io.github.wulkanowy.ui.modules.login.LoginActivity
import io.github.wulkanowy.ui.modules.main.MainActivity import io.github.wulkanowy.ui.modules.login.LoginData
import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.openEmailClient import io.github.wulkanowy.utils.openEmailClient
import io.github.wulkanowy.utils.openInternetBrowser import io.github.wulkanowy.utils.openInternetBrowser
import io.github.wulkanowy.utils.serializable
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
@ -36,12 +35,23 @@ class LoginStudentSelectFragment :
@Inject @Inject
lateinit var preferencesRepository: PreferencesRepository lateinit var preferencesRepository: PreferencesRepository
companion object { private lateinit var symbolsNames: Array<String>
const val ARG_STUDENTS = "STUDENTS" private lateinit var symbolsValues: Array<String>
fun newInstance(studentsWithSemesters: List<StudentWithSemesters>) = override val symbols: Map<String, String> by lazy {
symbolsValues.zip(symbolsNames).toMap()
}
companion object {
private const val ARG_LOGIN = "LOGIN"
private const val ARG_STUDENTS = "STUDENTS"
fun newInstance(loginData: LoginData, registerUser: RegisterUser) =
LoginStudentSelectFragment().apply { LoginStudentSelectFragment().apply {
arguments = bundleOf(ARG_STUDENTS to studentsWithSemesters) arguments = bundleOf(
ARG_LOGIN to loginData,
ARG_STUDENTS to registerUser,
)
} }
} }
@ -49,62 +59,50 @@ class LoginStudentSelectFragment :
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
binding = FragmentLoginStudentSelectBinding.bind(view) binding = FragmentLoginStudentSelectBinding.bind(view)
symbolsNames = resources.getStringArray(R.array.symbols)
symbolsValues = resources.getStringArray(R.array.symbols_values)
presenter.onAttachView( presenter.onAttachView(
view = this, view = this,
students = requireArguments().getSerializable(ARG_STUDENTS) as List<StudentWithSemesters>, loginData = requireArguments().serializable(ARG_LOGIN),
registerUser = requireArguments().serializable(ARG_STUDENTS),
) )
} }
override fun initView() { override fun initView() {
(requireActivity() as LoginActivity).showActionBar(true) (requireActivity() as LoginActivity).showActionBar(true)
loginAdapter.onClickListener = presenter::onItemSelected
with(binding) { with(binding) {
loginStudentSelectSignIn.setOnClickListener { presenter.onSignIn() } loginStudentSelectSignIn.setOnClickListener { presenter.onSignIn() }
loginStudentSelectContactDiscord.setOnClickListener { presenter.onDiscordClick() } loginStudentSelectRecycler.adapter = loginAdapter
loginStudentSelectContactEmail.setOnClickListener { presenter.onEmailClick() }
with(loginStudentSelectRecycler) {
layoutManager = LinearLayoutManager(context)
adapter = loginAdapter
}
} }
} }
override fun updateData(data: List<Pair<StudentWithSemesters, Boolean>>) { override fun updateData(data: List<LoginStudentSelectItem>) {
with(loginAdapter) { loginAdapter.submitList(data)
items = data
notifyDataSetChanged()
}
} }
override fun openMainView() { override fun navigateToSymbol(loginData: LoginData) {
startActivity(MainActivity.getStartIntent(requireContext())) (requireActivity() as LoginActivity).navigateToSymbolFragment(loginData)
requireActivity().finish() }
override fun navigateToNext() {
(requireActivity() as LoginActivity).navigateToNotifications()
} }
override fun showProgress(show: Boolean) { override fun showProgress(show: Boolean) {
binding.loginStudentSelectProgress.visibility = if (show) VISIBLE else GONE binding.loginStudentSelectProgress.isVisible = show
} }
override fun showContent(show: Boolean) { override fun showContent(show: Boolean) {
binding.loginStudentSelectContent.visibility = if (show) VISIBLE else GONE binding.loginStudentSelectContent.isVisible = show
} }
override fun enableSignIn(enable: Boolean) { override fun enableSignIn(enable: Boolean) {
binding.loginStudentSelectSignIn.isEnabled = enable binding.loginStudentSelectSignIn.isEnabled = enable
} }
override fun showContact(show: Boolean) {
binding.loginStudentSelectContact.visibility = if (show) VISIBLE else GONE
}
override fun onDestroyView() {
presenter.onDetachView()
super.onDestroyView()
}
override fun openDiscordInvite() { override fun openDiscordInvite() {
context?.openInternetBrowser("https://discord.gg/vccAQBr", ::showMessage) context?.openInternetBrowser("https://discord.gg/vccAQBr", ::showMessage)
} }
@ -125,4 +123,9 @@ class LoginStudentSelectFragment :
) )
) )
} }
override fun onDestroyView() {
presenter.onDetachView()
super.onDestroyView()
}
} }

View File

@ -0,0 +1,50 @@
package io.github.wulkanowy.ui.modules.login.studentselect
import io.github.wulkanowy.data.pojos.RegisterStudent
import io.github.wulkanowy.data.pojos.RegisterSymbol
import io.github.wulkanowy.data.pojos.RegisterUnit
sealed class LoginStudentSelectItem(val type: LoginStudentSelectItemType) {
data class EmptySymbolsHeader(
val isExpanded: Boolean,
val onClick: () -> Unit,
) : LoginStudentSelectItem(LoginStudentSelectItemType.EMPTY_SYMBOLS_HEADER)
data class SymbolHeader(
val symbol: RegisterSymbol,
val humanReadableName: String?,
val isErrorExpanded: Boolean,
val onClick: (RegisterSymbol) -> Unit,
) : LoginStudentSelectItem(LoginStudentSelectItemType.SYMBOL_HEADER)
data class SchoolHeader(
val unit: RegisterUnit,
val isErrorExpanded: Boolean,
val onClick: (RegisterUnit) -> Unit,
) : LoginStudentSelectItem(LoginStudentSelectItemType.SCHOOL_HEADER)
data class Student(
val symbol: RegisterSymbol,
val unit: RegisterUnit,
val student: RegisterStudent,
val isEnabled: Boolean,
val isSelected: Boolean,
val onClick: (Student) -> Unit,
) : LoginStudentSelectItem(LoginStudentSelectItemType.STUDENT)
data class Help(
val onEnterSymbolClick: () -> Unit,
val onContactUsClick: () -> Unit,
val onDiscordClick: () -> Unit,
val isSymbolButtonVisible: Boolean,
) : LoginStudentSelectItem(LoginStudentSelectItemType.HELP)
}
enum class LoginStudentSelectItemType {
EMPTY_SYMBOLS_HEADER,
SYMBOL_HEADER,
SCHOOL_HEADER,
STUDENT,
HELP,
}

View File

@ -1,15 +1,24 @@
package io.github.wulkanowy.ui.modules.login.studentselect package io.github.wulkanowy.ui.modules.login.studentselect
import io.github.wulkanowy.data.Resource import io.github.wulkanowy.data.Resource
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.dataOrNull
import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.data.logResourceStatus import io.github.wulkanowy.data.logResourceStatus
import io.github.wulkanowy.data.mappers.mapToStudentWithSemesters
import io.github.wulkanowy.data.pojos.RegisterStudent
import io.github.wulkanowy.data.pojos.RegisterSymbol
import io.github.wulkanowy.data.pojos.RegisterUnit
import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.resourceFlow import io.github.wulkanowy.data.resourceFlow
import io.github.wulkanowy.sdk.scrapper.login.AccountPermissionException
import io.github.wulkanowy.sdk.scrapper.login.InvalidSymbolException
import io.github.wulkanowy.services.sync.SyncManager import io.github.wulkanowy.services.sync.SyncManager
import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.modules.login.LoginData
import io.github.wulkanowy.ui.modules.login.LoginErrorHandler import io.github.wulkanowy.ui.modules.login.LoginErrorHandler
import io.github.wulkanowy.utils.AnalyticsHelper import io.github.wulkanowy.utils.AnalyticsHelper
import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.ifNullOrBlank import io.github.wulkanowy.utils.ifNullOrBlank
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import timber.log.Timber import timber.log.Timber
@ -19,18 +28,30 @@ class LoginStudentSelectPresenter @Inject constructor(
studentRepository: StudentRepository, studentRepository: StudentRepository,
private val loginErrorHandler: LoginErrorHandler, private val loginErrorHandler: LoginErrorHandler,
private val syncManager: SyncManager, private val syncManager: SyncManager,
private val analytics: AnalyticsHelper private val analytics: AnalyticsHelper,
private val appInfo: AppInfo,
) : BasePresenter<LoginStudentSelectView>(loginErrorHandler, studentRepository) { ) : BasePresenter<LoginStudentSelectView>(loginErrorHandler, studentRepository) {
private var lastError: Throwable? = null private var lastError: Throwable? = null
private val selectedStudents = mutableListOf<StudentWithSemesters>() private lateinit var registerUser: RegisterUser
private lateinit var loginData: LoginData
fun onAttachView(view: LoginStudentSelectView, students: List<StudentWithSemesters>) { private lateinit var students: List<StudentWithSemesters>
private var isEmptySymbolsExpanded = false
private var expandedSymbolError: RegisterSymbol? = null
private var expandedSchoolError: RegisterUnit? = null
private val selectedStudents = mutableListOf<LoginStudentSelectItem.Student>()
fun onAttachView(
view: LoginStudentSelectView,
loginData: LoginData,
registerUser: RegisterUser,
) {
super.onAttachView(view) super.onAttachView(view)
with(view) { with(view) {
initView() initView()
showContact(false)
enableSignIn(false) enableSignIn(false)
loginErrorHandler.onStudentDuplicate = { loginErrorHandler.onStudentDuplicate = {
showMessage(it) showMessage(it)
@ -38,50 +59,171 @@ class LoginStudentSelectPresenter @Inject constructor(
} }
} }
if (students.size == 1) registerStudents(students) this.loginData = loginData
loadData(students) this.registerUser = registerUser
loadData()
} }
private fun loadData() {
resetSelectedState()
resourceFlow { studentRepository.getSavedStudents(false) }.onEach {
students = it.dataOrNull.orEmpty()
when (it) {
is Resource.Loading -> Timber.d("Login student select students load started")
is Resource.Success -> refreshItems()
is Resource.Error -> {
errorHandler.dispatch(it.error)
lastError = it.error
refreshItems()
}
}
}.launch()
}
private fun createItems(): List<LoginStudentSelectItem> = buildList {
val notEmptySymbols = registerUser.symbols.filter { it.schools.isNotEmpty() }
val emptySymbols = registerUser.symbols.filter { it.schools.isEmpty() }
if (emptySymbols.isNotEmpty() && notEmptySymbols.isNotEmpty() && emptySymbols.any { it.symbol == loginData.symbol }) {
add(createEmptySymbolItem(emptySymbols.first { it.symbol == loginData.symbol }))
}
addAll(createNotEmptySymbolItems(notEmptySymbols, students))
addAll(createEmptySymbolItems(emptySymbols, notEmptySymbols.isNotEmpty()))
val helpItem = LoginStudentSelectItem.Help(
onEnterSymbolClick = ::onEnterSymbol,
onContactUsClick = ::onEmailClick,
onDiscordClick = ::onDiscordClick,
isSymbolButtonVisible = "login" !in loginData.baseUrl,
)
add(helpItem)
}
private fun createNotEmptySymbolItems(
notEmptySymbols: List<RegisterSymbol>,
students: List<StudentWithSemesters>,
) = buildList {
notEmptySymbols.forEach { registerSymbol ->
val symbolHeader = LoginStudentSelectItem.SymbolHeader(
symbol = registerSymbol,
humanReadableName = view?.symbols?.get(registerSymbol.symbol),
isErrorExpanded = expandedSymbolError == registerSymbol,
onClick = ::onSymbolItemClick,
)
add(symbolHeader)
registerSymbol.schools.forEach { registerUnit ->
val schoolHeader = LoginStudentSelectItem.SchoolHeader(
unit = registerUnit,
isErrorExpanded = expandedSchoolError == registerUnit,
onClick = ::onUnitItemClick,
)
add(schoolHeader)
registerUnit.students.forEach {
add(createStudentItem(it, registerSymbol, registerUnit, students))
}
}
}
}
private fun createStudentItem(
student: RegisterStudent,
symbol: RegisterSymbol,
school: RegisterUnit,
students: List<StudentWithSemesters>,
) = LoginStudentSelectItem.Student(
symbol = symbol,
unit = school,
student = student,
onClick = ::onItemSelected,
isEnabled = students.none {
it.student.email == registerUser.login
&& it.student.symbol == symbol.symbol
&& it.student.studentId == student.studentId
&& it.student.schoolSymbol == school.schoolId
&& it.student.classId == student.classId
},
isSelected = selectedStudents
.filter { it.symbol.symbol == symbol.symbol }
.filter { it.unit.schoolId == school.schoolId }
.filter { it.student.studentId == student.studentId }
.filter { it.student.classId == student.classId }
.size == 1,
)
private fun createEmptySymbolItems(
emptySymbols: List<RegisterSymbol>,
isNotEmptySymbolsExist: Boolean,
) = buildList {
val filteredEmptySymbols = emptySymbols.filter {
it.error !is InvalidSymbolException
}.ifEmpty { emptySymbols.takeIf { !isNotEmptySymbolsExist }.orEmpty() }
if (filteredEmptySymbols.isNotEmpty() && isNotEmptySymbolsExist) {
val emptyHeader = LoginStudentSelectItem.EmptySymbolsHeader(
isExpanded = isEmptySymbolsExpanded,
onClick = ::onEmptySymbolsToggle,
)
add(emptyHeader)
if (isEmptySymbolsExpanded) {
filteredEmptySymbols.forEach {
add(createEmptySymbolItem(it))
}
}
}
if (filteredEmptySymbols.isNotEmpty() && !isNotEmptySymbolsExist) {
filteredEmptySymbols.forEach {
add(createEmptySymbolItem(it))
}
}
}
private fun createEmptySymbolItem(registerSymbol: RegisterSymbol) =
LoginStudentSelectItem.SymbolHeader(
symbol = registerSymbol,
humanReadableName = view?.symbols?.get(registerSymbol.symbol),
isErrorExpanded = expandedSymbolError == registerSymbol,
onClick = ::onSymbolItemClick,
)
fun onSignIn() { fun onSignIn() {
registerStudents(selectedStudents) registerStudents(selectedStudents)
} }
fun onItemSelected(studentWithSemester: StudentWithSemesters, alreadySaved: Boolean) { private fun onEmptySymbolsToggle() {
if (alreadySaved) return isEmptySymbolsExpanded = !isEmptySymbolsExpanded
refreshItems()
}
private fun onItemSelected(item: LoginStudentSelectItem.Student) {
if (!item.isEnabled) return
selectedStudents selectedStudents
.removeAll { it == studentWithSemester } .removeAll {
.let { if (!it) selectedStudents.add(studentWithSemester) } it.student.studentId == item.student.studentId &&
it.student.classId == item.student.classId &&
it.unit.schoolId == item.unit.schoolId &&
it.symbol.symbol == item.symbol.symbol
}
.let { if (!it) selectedStudents.add(item) }
view?.enableSignIn(selectedStudents.isNotEmpty()) view?.enableSignIn(selectedStudents.isNotEmpty())
refreshItems()
} }
private fun compareStudents(a: Student, b: Student): Boolean { private fun onSymbolItemClick(symbol: RegisterSymbol) {
return a.email == b.email expandedSymbolError = if (symbol != expandedSymbolError) symbol else null
&& a.symbol == b.symbol refreshItems()
&& a.studentId == b.studentId
&& a.schoolSymbol == b.schoolSymbol
&& a.classId == b.classId
} }
private fun loadData(studentsWithSemesters: List<StudentWithSemesters>) { private fun onUnitItemClick(unit: RegisterUnit) {
resetSelectedState() expandedSchoolError = if (unit != expandedSchoolError) unit else null
refreshItems()
resourceFlow { studentRepository.getSavedStudents(false) }.onEach {
when (it) {
is Resource.Loading -> Timber.d("Login student select students load started")
is Resource.Success -> view?.updateData(studentsWithSemesters.map { studentWithSemesters ->
studentWithSemesters to it.data.any { item ->
compareStudents(studentWithSemesters.student, item.student)
}
})
is Resource.Error -> {
errorHandler.dispatch(it.error)
lastError = it.error
view?.updateData(studentsWithSemesters.map { student -> student to false })
}
}
}.launch()
} }
private fun resetSelectedState() { private fun resetSelectedState() {
@ -89,7 +231,20 @@ class LoginStudentSelectPresenter @Inject constructor(
view?.enableSignIn(false) view?.enableSignIn(false)
} }
private fun registerStudents(studentsWithSemesters: List<StudentWithSemesters>) { private fun refreshItems() {
view?.updateData(createItems())
}
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) } resourceFlow { studentRepository.saveStudents(studentsWithSemesters) }
.logResourceStatus("registration") .logResourceStatus("registration")
.onEach { .onEach {
@ -100,14 +255,13 @@ class LoginStudentSelectPresenter @Inject constructor(
} }
is Resource.Success -> { is Resource.Success -> {
syncManager.startOneTimeSyncWorker(quiet = true) syncManager.startOneTimeSyncWorker(quiet = true)
view?.openMainView() view?.navigateToNext()
logRegisterEvent(studentsWithSemesters) logRegisterEvent(studentsWithSemesters)
} }
is Resource.Error -> { is Resource.Error -> {
view?.apply { view?.apply {
showProgress(false) showProgress(false)
showContent(true) showContent(true)
showContact(true)
} }
lastError = it.error lastError = it.error
loginErrorHandler.dispatch(it.error) loginErrorHandler.dispatch(it.error)
@ -117,12 +271,37 @@ class LoginStudentSelectPresenter @Inject constructor(
}.launch("register") }.launch("register")
} }
fun onDiscordClick() { private fun onEnterSymbol() {
view?.navigateToSymbol(loginData)
}
private fun onDiscordClick() {
view?.openDiscordInvite() view?.openDiscordInvite()
} }
fun onEmailClick() { private fun onEmailClick() {
view?.openEmail(lastError?.message.ifNullOrBlank { "empty" }) 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 }
})
} }
private fun logRegisterEvent( private fun logRegisterEvent(

View File

@ -1,15 +1,19 @@
package io.github.wulkanowy.ui.modules.login.studentselect package io.github.wulkanowy.ui.modules.login.studentselect
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.ui.base.BaseView import io.github.wulkanowy.ui.base.BaseView
import io.github.wulkanowy.ui.modules.login.LoginData
interface LoginStudentSelectView : BaseView { interface LoginStudentSelectView : BaseView {
val symbols: Map<String, String>
fun initView() fun initView()
fun updateData(data: List<Pair<StudentWithSemesters, Boolean>>) fun updateData(data: List<LoginStudentSelectItem>)
fun openMainView() fun navigateToSymbol(loginData: LoginData)
fun navigateToNext()
fun showProgress(show: Boolean) fun showProgress(show: Boolean)
@ -17,8 +21,6 @@ interface LoginStudentSelectView : BaseView {
fun enableSignIn(enable: Boolean) fun enableSignIn(enable: Boolean)
fun showContact(show: Boolean)
fun openDiscordInvite() fun openDiscordInvite()
fun openEmail(lastError: String) fun openEmail(lastError: String)

View File

@ -12,17 +12,13 @@ import androidx.core.text.parseAsHtml
import androidx.core.widget.doOnTextChanged import androidx.core.widget.doOnTextChanged
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.databinding.FragmentLoginSymbolBinding import io.github.wulkanowy.databinding.FragmentLoginSymbolBinding
import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.login.LoginActivity import io.github.wulkanowy.ui.modules.login.LoginActivity
import io.github.wulkanowy.ui.modules.login.LoginData import io.github.wulkanowy.ui.modules.login.LoginData
import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.*
import io.github.wulkanowy.utils.hideSoftInput
import io.github.wulkanowy.utils.openEmailClient
import io.github.wulkanowy.utils.openInternetBrowser
import io.github.wulkanowy.utils.showSoftInput
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
@ -46,6 +42,8 @@ class LoginSymbolFragment :
} }
} }
override val symbolValue: String? get() = binding.loginSymbolName.text?.toString()
override val symbolNameError: CharSequence? override val symbolNameError: CharSequence?
get() = binding.loginSymbolNameLayout.error get() = binding.loginSymbolNameLayout.error
@ -54,7 +52,7 @@ class LoginSymbolFragment :
binding = FragmentLoginSymbolBinding.bind(view) binding = FragmentLoginSymbolBinding.bind(view)
presenter.onAttachView( presenter.onAttachView(
view = this, view = this,
loginData = requireArguments().getSerializable(SAVED_LOGIN_DATA) as LoginData, loginData = requireArguments().serializable(SAVED_LOGIN_DATA),
) )
} }
@ -62,7 +60,7 @@ class LoginSymbolFragment :
(requireActivity() as LoginActivity).showActionBar(true) (requireActivity() as LoginActivity).showActionBar(true)
with(binding) { with(binding) {
loginSymbolSignIn.setOnClickListener { presenter.attemptLogin(loginSymbolName.text.toString()) } loginSymbolSignIn.setOnClickListener { presenter.attemptLogin() }
loginSymbolFaq.setOnClickListener { presenter.onFaqClick() } loginSymbolFaq.setOnClickListener { presenter.onFaqClick() }
loginSymbolContactEmail.setOnClickListener { presenter.onEmailClick() } loginSymbolContactEmail.setOnClickListener { presenter.onEmailClick() }
@ -95,10 +93,21 @@ class LoginSymbolFragment :
} }
} }
override fun setErrorSymbolRequire() { override fun setErrorSymbolInvalid() {
binding.loginSymbolNameLayout.apply { with(binding.loginSymbolNameLayout) {
requestFocus() requestFocus()
error = getString(R.string.error_field_required) error = getString(R.string.login_invalid_symbol)
}
}
override fun setErrorSymbolRequire() {
setErrorSymbol(getString(R.string.error_field_required))
}
override fun setErrorSymbol(message: String) {
with(binding.loginSymbolNameLayout) {
requestFocus()
error = message
} }
} }
@ -129,8 +138,8 @@ class LoginSymbolFragment :
binding.loginSymbolContainer.visibility = if (show) VISIBLE else GONE binding.loginSymbolContainer.visibility = if (show) VISIBLE else GONE
} }
override fun navigateToStudentSelect(studentsWithSemesters: List<StudentWithSemesters>) { override fun navigateToStudentSelect(loginData: LoginData, registerUser: RegisterUser) {
(activity as? LoginActivity)?.navigateToStudentSelect(studentsWithSemesters) (activity as? LoginActivity)?.navigateToStudentSelect(loginData, registerUser)
} }
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {

View File

@ -1,9 +1,13 @@
package io.github.wulkanowy.ui.modules.login.symbol package io.github.wulkanowy.ui.modules.login.symbol
import io.github.wulkanowy.data.Resource import io.github.wulkanowy.data.Resource
import io.github.wulkanowy.data.dataOrNull
import io.github.wulkanowy.data.onResourceNotLoading import io.github.wulkanowy.data.onResourceNotLoading
import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.resourceFlow import io.github.wulkanowy.data.resourceFlow
import io.github.wulkanowy.sdk.scrapper.getNormalizedSymbol
import io.github.wulkanowy.sdk.scrapper.login.InvalidSymbolException
import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.modules.login.LoginData import io.github.wulkanowy.ui.modules.login.LoginData
import io.github.wulkanowy.ui.modules.login.LoginErrorHandler import io.github.wulkanowy.ui.modules.login.LoginErrorHandler
@ -23,9 +27,14 @@ class LoginSymbolPresenter @Inject constructor(
lateinit var loginData: LoginData lateinit var loginData: LoginData
private var registerUser: RegisterUser? = null
fun onAttachView(view: LoginSymbolView, loginData: LoginData) { fun onAttachView(view: LoginSymbolView, loginData: LoginData) {
super.onAttachView(view) super.onAttachView(view)
this.loginData = loginData this.loginData = loginData
loginErrorHandler.onBadCredentials = {
view.setErrorSymbol(it.orEmpty())
}
with(view) { with(view) {
initView() initView()
showContact(false) showContact(false)
@ -39,21 +48,25 @@ class LoginSymbolPresenter @Inject constructor(
view?.apply { if (symbolNameError != null) clearSymbolError() } view?.apply { if (symbolNameError != null) clearSymbolError() }
} }
fun attemptLogin(symbol: String) { fun attemptLogin() {
if (symbol.isBlank()) { if (view?.symbolValue.isNullOrBlank()) {
view?.setErrorSymbolRequire() view?.setErrorSymbolRequire()
return return
} }
loginData = loginData.copy(
symbol = view?.symbolValue?.getNormalizedSymbol(),
)
resourceFlow { resourceFlow {
studentRepository.getStudentsScrapper( studentRepository.getUserSubjectsFromScrapper(
email = loginData.login, email = loginData.login,
password = loginData.password, password = loginData.password,
scrapperBaseUrl = loginData.baseUrl, scrapperBaseUrl = loginData.baseUrl,
symbol = symbol, symbol = loginData.symbol.orEmpty(),
) )
}.onEach { }.onEach { user ->
when (it) { registerUser = user.dataOrNull
when (user) {
is Resource.Loading -> view?.run { is Resource.Loading -> view?.run {
Timber.i("Login with symbol started") Timber.i("Login with symbol started")
hideSoftKeyboard() hideSoftKeyboard()
@ -61,7 +74,7 @@ class LoginSymbolPresenter @Inject constructor(
showContent(false) showContent(false)
} }
is Resource.Success -> { is Resource.Success -> {
when (it.data.size) { when (user.data.symbols.size) {
0 -> { 0 -> {
Timber.i("Login with symbol result: Empty student list") Timber.i("Login with symbol result: Empty student list")
view?.run { view?.run {
@ -70,16 +83,26 @@ class LoginSymbolPresenter @Inject constructor(
} }
} }
else -> { else -> {
Timber.i("Login with symbol result: Success") val enteredSymbolDetails = user.data.symbols
view?.navigateToStudentSelect(requireNotNull(it.data)) .firstOrNull()
?.takeIf { it.symbol == loginData.symbol }
if (enteredSymbolDetails?.error is InvalidSymbolException) {
view?.run {
setErrorSymbolInvalid()
showContact(true)
}
} else {
Timber.i("Login with symbol result: Success")
view?.navigateToStudentSelect(loginData, requireNotNull(user.data))
}
} }
} }
analytics.logEvent( analytics.logEvent(
"registration_symbol", "registration_symbol",
"success" to true, "success" to true,
"students" to it.data.size,
"scrapperBaseUrl" to loginData.baseUrl, "scrapperBaseUrl" to loginData.baseUrl,
"symbol" to symbol, "symbol" to view?.symbolValue,
"error" to "No error" "error" to "No error"
) )
} }
@ -90,11 +113,11 @@ class LoginSymbolPresenter @Inject constructor(
"success" to false, "success" to false,
"students" to -1, "students" to -1,
"scrapperBaseUrl" to loginData.baseUrl, "scrapperBaseUrl" to loginData.baseUrl,
"symbol" to symbol, "symbol" to view?.symbolValue,
"error" to it.error.message.ifNullOrBlank { "No message" } "error" to user.error.message.ifNullOrBlank { "No message" }
) )
loginErrorHandler.dispatch(it.error) loginErrorHandler.dispatch(user.error)
lastError = it.error lastError = user.error
view?.showContact(true) view?.showContact(true)
} }
} }
@ -111,6 +134,12 @@ class LoginSymbolPresenter @Inject constructor(
} }
fun onEmailClick() { fun onEmailClick() {
view?.openEmail(loginData.baseUrl, lastError?.message.ifNullOrBlank { "empty" }) 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 File

@ -1,10 +1,13 @@
package io.github.wulkanowy.ui.modules.login.symbol package io.github.wulkanowy.ui.modules.login.symbol
import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.ui.base.BaseView import io.github.wulkanowy.ui.base.BaseView
import io.github.wulkanowy.ui.modules.login.LoginData
interface LoginSymbolView : BaseView { interface LoginSymbolView : BaseView {
val symbolValue: String?
val symbolNameError: CharSequence? val symbolNameError: CharSequence?
fun initView() fun initView()
@ -13,8 +16,12 @@ interface LoginSymbolView : BaseView {
fun setErrorSymbolIncorrect() fun setErrorSymbolIncorrect()
fun setErrorSymbolInvalid()
fun setErrorSymbolRequire() fun setErrorSymbolRequire()
fun setErrorSymbol(message: String)
fun clearSymbolError() fun clearSymbolError()
fun clearAndFocusSymbol() fun clearAndFocusSymbol()
@ -27,7 +34,7 @@ interface LoginSymbolView : BaseView {
fun showContent(show: Boolean) fun showContent(show: Boolean)
fun navigateToStudentSelect(studentsWithSemesters: List<StudentWithSemesters>) fun navigateToStudentSelect(loginData: LoginData, registerUser: RegisterUser)
fun showContact(show: Boolean) fun showContact(show: Boolean)

View File

@ -6,6 +6,8 @@ import android.os.Build.VERSION_CODES.P
import android.os.Bundle import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import androidx.activity.OnBackPressedCallback
import androidx.activity.addCallback
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
@ -50,6 +52,8 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
@Inject @Inject
lateinit var appInfo: AppInfo lateinit var appInfo: AppInfo
private var onBackCallback: OnBackPressedCallback? = null
private var accountMenu: MenuItem? = null private var accountMenu: MenuItem? = null
private val overlayProvider by lazy { ElevationOverlayProvider(this) } private val overlayProvider by lazy { ElevationOverlayProvider(this) }
@ -88,6 +92,9 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
this.savedInstanceState = savedInstanceState this.savedInstanceState = savedInstanceState
messageContainer = binding.mainMessageContainer messageContainer = binding.mainMessageContainer
updateHelper.messageContainer = binding.mainFragmentContainer updateHelper.messageContainer = binding.mainFragmentContainer
onBackCallback = onBackPressedDispatcher.addCallback(this, enabled = false) {
presenter.onBackPressed()
}
val destination = intent.getStringExtra(EXTRA_START_DESTINATION) val destination = intent.getStringExtra(EXTRA_START_DESTINATION)
?.takeIf { savedInstanceState == null } ?.takeIf { savedInstanceState == null }
@ -266,6 +273,7 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
analytics.popCurrentScreen(navController.currentFrag!!::class.simpleName) analytics.popCurrentScreen(navController.currentFrag!!::class.simpleName)
navController.pushFragment(fragment) navController.pushFragment(fragment)
onBackCallback?.isEnabled = !isRootView
} }
override fun popView(depth: Int) { override fun popView(depth: Int) {
@ -273,10 +281,7 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
analytics.popCurrentScreen(navController.currentFrag!!::class.simpleName) analytics.popCurrentScreen(navController.currentFrag!!::class.simpleName)
navController.safelyPopFragments(depth) navController.safelyPopFragments(depth)
} onBackCallback?.isEnabled = !isRootView
override fun onBackPressed() {
presenter.onBackPressed { super.onBackPressed() }
} }
override fun showStudentAvatar(student: Student) { override fun showStudentAvatar(student: Student) {

View File

@ -139,12 +139,9 @@ class MainPresenter @Inject constructor(
return true return true
} }
fun onBackPressed(default: () -> Unit) { fun onBackPressed() {
Timber.i("Back pressed in main view") Timber.i("Back pressed in main view")
view?.run { view?.popView()
if (isRootView) default()
else popView()
}
} }
fun onTabSelected(index: Int, wasSelected: Boolean): Boolean { fun onTabSelected(index: Int, wasSelected: Boolean): Boolean {

View File

@ -10,6 +10,7 @@ import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.data.db.entities.Mailbox import io.github.wulkanowy.data.db.entities.Mailbox
import io.github.wulkanowy.databinding.DialogMailboxChooserBinding import io.github.wulkanowy.databinding.DialogMailboxChooserBinding
import io.github.wulkanowy.ui.base.BaseDialogFragment import io.github.wulkanowy.ui.base.BaseDialogFragment
import io.github.wulkanowy.utils.parcelableArray
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
@ -52,8 +53,7 @@ class MailboxChooserDialog : BaseDialogFragment<DialogMailboxChooserBinding>(),
presenter.onAttachView( presenter.onAttachView(
view = this, view = this,
requireMailbox = requireArguments().getBoolean(REQUIRED_KEY, false), requireMailbox = requireArguments().getBoolean(REQUIRED_KEY, false),
mailboxes = requireArguments().getParcelableArray(MAILBOX_KEY).orEmpty() mailboxes = requireArguments().parcelableArray<Mailbox>(MAILBOX_KEY).orEmpty().toList(),
.toList() as List<Mailbox>,
) )
} }

View File

@ -13,6 +13,7 @@ import android.webkit.WebResourceRequest
import android.webkit.WebView import android.webkit.WebView
import android.webkit.WebViewClient import android.webkit.WebViewClient
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import androidx.core.os.bundleOf
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R import io.github.wulkanowy.R
@ -23,6 +24,7 @@ import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.main.MainActivity import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.ui.modules.message.send.SendMessageActivity import io.github.wulkanowy.ui.modules.message.send.SendMessageActivity
import io.github.wulkanowy.utils.serializable
import io.github.wulkanowy.utils.shareText import io.github.wulkanowy.utils.shareText
import javax.inject.Inject import javax.inject.Inject
@ -66,13 +68,12 @@ class MessagePreviewFragment :
companion object { companion object {
const val MESSAGE_ID_KEY = "message_id" const val MESSAGE_ID_KEY = "message_id"
fun newInstance(message: Message): MessagePreviewFragment { fun newInstance(message: Message) = MessagePreviewFragment().apply {
return MessagePreviewFragment().apply { arguments = bundleOf(MESSAGE_ID_KEY to message)
arguments = Bundle().apply { putSerializable(MESSAGE_ID_KEY, message) }
}
} }
} }
@Suppress("DEPRECATION")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setHasOptionsMenu(true) setHasOptionsMenu(true)
@ -83,8 +84,8 @@ class MessagePreviewFragment :
binding = FragmentMessagePreviewBinding.bind(view) binding = FragmentMessagePreviewBinding.bind(view)
messageContainer = binding.messagePreviewContainer messageContainer = binding.messagePreviewContainer
presenter.onAttachView( presenter.onAttachView(
this, view = this,
(savedInstanceState ?: arguments)?.getSerializable(MESSAGE_ID_KEY) as? Message message = (savedInstanceState ?: arguments)?.serializable(MESSAGE_ID_KEY),
) )
} }

View File

@ -186,7 +186,7 @@ class MessagePreviewPresenter @Inject constructor(
runCatching { runCatching {
val student = studentRepository.getCurrentStudent(decryptPass = true) val student = studentRepository.getCurrentStudent(decryptPass = true)
val mailbox = messageRepository.getMailboxByStudent(student) val mailbox = messageRepository.getMailboxByStudent(student)
messageRepository.deleteMessage(student, mailbox!!, message!!) messageRepository.deleteMessage(student, mailbox, message!!)
} }
.onFailure { .onFailure {
retryCallback = { onMessageDelete() } retryCallback = { onMessageDelete() }

View File

@ -28,6 +28,7 @@ import io.github.wulkanowy.ui.modules.message.mailboxchooser.MailboxChooserDialo
import io.github.wulkanowy.ui.modules.message.mailboxchooser.MailboxChooserDialog.Companion.LISTENER_KEY import io.github.wulkanowy.ui.modules.message.mailboxchooser.MailboxChooserDialog.Companion.LISTENER_KEY
import io.github.wulkanowy.utils.dpToPx import io.github.wulkanowy.utils.dpToPx
import io.github.wulkanowy.utils.hideSoftInput import io.github.wulkanowy.utils.hideSoftInput
import io.github.wulkanowy.utils.nullableSerializable
import io.github.wulkanowy.utils.showSoftInput import io.github.wulkanowy.utils.showSoftInput
import javax.inject.Inject import javax.inject.Inject
@ -108,12 +109,12 @@ class SendMessageActivity : BaseActivity<SendMessagePresenter, ActivitySendMessa
presenter.onAttachView( presenter.onAttachView(
view = this, view = this,
reason = intent.getSerializableExtra(EXTRA_REASON) as? String, reason = intent.nullableSerializable(EXTRA_REASON),
message = intent.getSerializableExtra(EXTRA_MESSAGE) as? Message, message = intent.nullableSerializable(EXTRA_MESSAGE),
reply = intent.getSerializableExtra(EXTRA_REPLY) as? Boolean reply = intent.nullableSerializable(EXTRA_REPLY)
) )
supportFragmentManager.setFragmentResultListener(LISTENER_KEY, this) { _, bundle -> supportFragmentManager.setFragmentResultListener(LISTENER_KEY, this) { _, bundle ->
presenter.onMailboxSelected(bundle.getSerializable(MAILBOX_KEY) as? Mailbox) presenter.onMailboxSelected(bundle.nullableSerializable(MAILBOX_KEY))
} }
} }

View File

@ -9,6 +9,7 @@ import android.view.View.*
import android.widget.CompoundButton import android.widget.CompoundButton
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.core.os.bundleOf
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.fragment.app.setFragmentResultListener import androidx.fragment.app.setFragmentResultListener
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
@ -27,6 +28,7 @@ import io.github.wulkanowy.ui.widgets.DividerItemDecoration
import io.github.wulkanowy.utils.dpToPx import io.github.wulkanowy.utils.dpToPx
import io.github.wulkanowy.utils.getThemeAttrColor import io.github.wulkanowy.utils.getThemeAttrColor
import io.github.wulkanowy.utils.hideSoftInput import io.github.wulkanowy.utils.hideSoftInput
import io.github.wulkanowy.utils.nullableSerializable
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
@ -43,12 +45,8 @@ class MessageTabFragment : BaseFragment<FragmentMessageTabBinding>(R.layout.frag
const val MESSAGE_TAB_FOLDER_ID = "message_tab_folder_id" const val MESSAGE_TAB_FOLDER_ID = "message_tab_folder_id"
fun newInstance(folder: MessageFolder): MessageTabFragment { fun newInstance(folder: MessageFolder) = MessageTabFragment().apply {
return MessageTabFragment().apply { arguments = bundleOf(MESSAGE_TAB_FOLDER_ID to folder.name)
arguments = Bundle().apply {
putString(MESSAGE_TAB_FOLDER_ID, folder.name)
}
}
} }
} }
@ -86,6 +84,7 @@ class MessageTabFragment : BaseFragment<FragmentMessageTabBinding>(R.layout.frag
} }
} }
@Suppress("DEPRECATION")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setHasOptionsMenu(true) setHasOptionsMenu(true)
@ -130,11 +129,12 @@ class MessageTabFragment : BaseFragment<FragmentMessageTabBinding>(R.layout.frag
setFragmentResultListener(requireArguments().getString(MESSAGE_TAB_FOLDER_ID)!!) { _, bundle -> setFragmentResultListener(requireArguments().getString(MESSAGE_TAB_FOLDER_ID)!!) { _, bundle ->
presenter.onMailboxSelected( presenter.onMailboxSelected(
mailbox = bundle.getSerializable(MailboxChooserDialog.MAILBOX_KEY) as? Mailbox, mailbox = bundle.nullableSerializable(MailboxChooserDialog.MAILBOX_KEY),
) )
} }
} }
@Suppress("DEPRECATION")
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater) super.onCreateOptionsMenu(menu, inflater)
inflater.inflate(R.menu.action_menu_message_tab, menu) inflater.inflate(R.menu.action_menu_message_tab, menu)

View File

@ -6,6 +6,7 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.os.bundleOf
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Note import io.github.wulkanowy.data.db.entities.Note
@ -13,6 +14,7 @@ import io.github.wulkanowy.databinding.DialogNoteBinding
import io.github.wulkanowy.sdk.scrapper.notes.NoteCategory import io.github.wulkanowy.sdk.scrapper.notes.NoteCategory
import io.github.wulkanowy.utils.getThemeAttrColor import io.github.wulkanowy.utils.getThemeAttrColor
import io.github.wulkanowy.utils.lifecycleAwareVariable import io.github.wulkanowy.utils.lifecycleAwareVariable
import io.github.wulkanowy.utils.serializable
import io.github.wulkanowy.utils.toFormattedString import io.github.wulkanowy.utils.toFormattedString
class NoteDialog : DialogFragment() { class NoteDialog : DialogFragment() {
@ -25,17 +27,15 @@ class NoteDialog : DialogFragment() {
private const val ARGUMENT_KEY = "Item" private const val ARGUMENT_KEY = "Item"
fun newInstance(exam: Note) = NoteDialog().apply { fun newInstance(note: Note) = NoteDialog().apply {
arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, exam) } arguments = bundleOf(ARGUMENT_KEY to note)
} }
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setStyle(STYLE_NO_TITLE, 0) setStyle(STYLE_NO_TITLE, 0)
arguments?.run { note = requireArguments().serializable(ARGUMENT_KEY)
note = getSerializable(ARGUMENT_KEY) as Note
}
} }
override fun onCreateView( override fun onCreateView(

View File

@ -0,0 +1,64 @@
package io.github.wulkanowy.ui.modules.notifications
import android.os.Bundle
import android.view.View
import androidx.activity.result.contract.ActivityResultContracts.RequestPermission
import androidx.appcompat.app.AlertDialog
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.databinding.FragmentNotificationsBinding
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.login.LoginActivity
import io.github.wulkanowy.utils.openNotificationSettings
@AndroidEntryPoint
class NotificationsFragment :
BaseFragment<FragmentNotificationsBinding>(R.layout.fragment_notifications) {
private val permission = "android.permission.POST_NOTIFICATIONS"
private val requestPermissionLauncher = registerForActivityResult(RequestPermission()) {
if (it) {
navigateToFinish()
} else showSettingsDialog()
}
companion object {
fun newInstance() = NotificationsFragment()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentNotificationsBinding.bind(view)
initView()
}
private fun initView() {
with(binding) {
notificationsSkip.setOnClickListener { navigateToFinish() }
notificationsEnable.setOnClickListener { requestPermission() }
}
}
private fun showSettingsDialog() {
AlertDialog.Builder(requireContext())
.setTitle(R.string.notifications_header_title)
.setMessage(R.string.notifications_header_description)
.setNegativeButton(R.string.notifications_skip) { dialog, _ ->
dialog.dismiss()
navigateToFinish()
}
.setPositiveButton(R.string.pref_notification_go_to_settings) { _, _ ->
requireActivity().openNotificationSettings()
}
.show()
}
private fun requestPermission() {
requestPermissionLauncher.launch(permission)
}
private fun navigateToFinish() {
(requireActivity() as LoginActivity).navigateToFinish()
}
}

View File

@ -2,10 +2,10 @@ package io.github.wulkanowy.ui.modules.schoolannouncement
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.text.parseAsHtml
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import io.github.wulkanowy.data.db.entities.SchoolAnnouncement import io.github.wulkanowy.data.db.entities.SchoolAnnouncement
import io.github.wulkanowy.databinding.ItemSchoolAnnouncementBinding import io.github.wulkanowy.databinding.ItemSchoolAnnouncementBinding
import io.github.wulkanowy.utils.parseUonetHtml
import io.github.wulkanowy.utils.toFormattedString import io.github.wulkanowy.utils.toFormattedString
import javax.inject.Inject import javax.inject.Inject
@ -28,7 +28,7 @@ class SchoolAnnouncementAdapter @Inject constructor() :
with(holder.binding) { with(holder.binding) {
schoolAnnouncementItemDate.text = item.date.toFormattedString() schoolAnnouncementItemDate.text = item.date.toFormattedString()
schoolAnnouncementItemType.text = item.subject schoolAnnouncementItemType.text = item.subject
schoolAnnouncementItemContent.text = item.content.parseAsHtml() schoolAnnouncementItemContent.text = item.content.parseUonetHtml()
root.setOnClickListener { onItemClickListener(item) } root.setOnClickListener { onItemClickListener(item) }
} }

View File

@ -4,11 +4,13 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.text.parseAsHtml import androidx.core.os.bundleOf
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import io.github.wulkanowy.data.db.entities.SchoolAnnouncement import io.github.wulkanowy.data.db.entities.SchoolAnnouncement
import io.github.wulkanowy.databinding.DialogSchoolAnnouncementBinding import io.github.wulkanowy.databinding.DialogSchoolAnnouncementBinding
import io.github.wulkanowy.utils.lifecycleAwareVariable import io.github.wulkanowy.utils.lifecycleAwareVariable
import io.github.wulkanowy.utils.parseUonetHtml
import io.github.wulkanowy.utils.serializable
import io.github.wulkanowy.utils.toFormattedString import io.github.wulkanowy.utils.toFormattedString
class SchoolAnnouncementDialog : DialogFragment() { class SchoolAnnouncementDialog : DialogFragment() {
@ -21,17 +23,15 @@ class SchoolAnnouncementDialog : DialogFragment() {
private const val ARGUMENT_KEY = "item" private const val ARGUMENT_KEY = "item"
fun newInstance(exam: SchoolAnnouncement) = SchoolAnnouncementDialog().apply { fun newInstance(announcement: SchoolAnnouncement) = SchoolAnnouncementDialog().apply {
arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, exam) } arguments = bundleOf(ARGUMENT_KEY to announcement)
} }
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setStyle(STYLE_NO_TITLE, 0) setStyle(STYLE_NO_TITLE, 0)
arguments?.run { announcement = requireArguments().serializable(ARGUMENT_KEY)
announcement = getSerializable(ARGUMENT_KEY) as SchoolAnnouncement
}
} }
override fun onCreateView( override fun onCreateView(
@ -46,7 +46,7 @@ class SchoolAnnouncementDialog : DialogFragment() {
with(binding) { with(binding) {
announcementDialogSubjectValue.text = announcement.subject announcementDialogSubjectValue.text = announcement.subject
announcementDialogDateValue.text = announcement.date.toFormattedString() announcementDialogDateValue.text = announcement.date.toFormattedString()
announcementDialogDescriptionValue.text = announcement.content.parseAsHtml() announcementDialogDescriptionValue.text = announcement.content.parseUonetHtml()
announcementDialogClose.setOnClickListener { dismiss() } announcementDialogClose.setOnClickListener { dismiss() }
} }

View File

@ -1,18 +1,16 @@
package io.github.wulkanowy.ui.modules.settings.notifications package io.github.wulkanowy.ui.modules.settings.notifications
import android.annotation.SuppressLint
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.net.Uri import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.provider.Settings
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreferenceCompat import androidx.preference.SwitchPreferenceCompat
@ -26,7 +24,7 @@ import io.github.wulkanowy.ui.base.ErrorDialog
import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.openInternetBrowser import io.github.wulkanowy.utils.openInternetBrowser
import timber.log.Timber import io.github.wulkanowy.utils.openNotificationSettings
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
@ -42,7 +40,14 @@ class NotificationsFragment : PreferenceFragmentCompat(),
override val titleStringId get() = R.string.pref_settings_notifications_title override val titleStringId get() = R.string.pref_settings_notifications_title
private val notificationsPermission = "android.permission.POST_NOTIFICATIONS"
override val isNotificationPermissionGranted: Boolean override val isNotificationPermissionGranted: Boolean
get() = ContextCompat.checkSelfPermission(
requireContext(), notificationsPermission
) == PackageManager.PERMISSION_GRANTED
override val isNotificationPiggybackPermissionGranted: Boolean
get() { get() {
val packageNameList = val packageNameList =
NotificationManagerCompat.getEnabledListenerPackages(requireContext()) NotificationManagerCompat.getEnabledListenerPackages(requireContext())
@ -51,6 +56,13 @@ class NotificationsFragment : PreferenceFragmentCompat(),
return appPackageName in packageNameList return appPackageName in packageNameList
} }
private val requestPermissionLauncher =
registerForActivityResult(ActivityResultContracts.RequestPermission()) {
if (it) {
presenter.onNotificationsPermissionResult()
} else openNotificationsPermissionDialog()
}
private val notificationSettingsPiggybackContract = private val notificationSettingsPiggybackContract =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
presenter.onNotificationPiggybackPermissionResult() presenter.onNotificationPiggybackPermissionResult()
@ -156,25 +168,29 @@ class NotificationsFragment : PreferenceFragmentCompat(),
.show() .show()
} }
@SuppressLint("InlinedApi")
override fun openSystemSettings() { override fun openSystemSettings() {
val intent = if (appInfo.systemVersion >= Build.VERSION_CODES.O) { requireActivity().openNotificationSettings()
Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply {
putExtra("android.provider.extra.APP_PACKAGE", requireActivity().packageName)
}
} else {
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.fromParts("package", requireActivity().packageName, null)
}
}
try {
requireActivity().startActivity(intent)
} catch (e: Exception) {
Timber.e(e)
}
} }
override fun openNotificationPermissionDialog() { override fun requestNotificationPermissions() {
requestPermissionLauncher.launch(notificationsPermission)
}
override fun openNotificationsPermissionDialog() {
AlertDialog.Builder(requireContext())
.setTitle(R.string.notifications_header_title)
.setMessage(R.string.notifications_header_description)
.setPositiveButton(R.string.pref_notification_go_to_settings) { _, _ ->
requireActivity().openNotificationSettings()
}
.setNegativeButton(android.R.string.cancel) { _, _ ->
setNotificationPreferencesChecked(false)
}
.setOnDismissListener { setNotificationPreferencesChecked(false) }
.show()
}
override fun openNotificationPiggyBackPermissionDialog() {
AlertDialog.Builder(requireContext()) AlertDialog.Builder(requireContext())
.setTitle(getString(R.string.pref_notification_piggyback_popup_title)) .setTitle(getString(R.string.pref_notification_piggyback_popup_title))
.setMessage(getString(R.string.pref_notification_piggyback_popup_description)) .setMessage(getString(R.string.pref_notification_piggyback_popup_description))
@ -202,6 +218,11 @@ class NotificationsFragment : PreferenceFragmentCompat(),
.show() .show()
} }
override fun setNotificationPreferencesChecked(isChecked: Boolean) {
findPreference<SwitchPreferenceCompat>(getString(R.string.pref_key_notifications_enable))?.isChecked =
isChecked
}
override fun setNotificationPiggybackPreferenceChecked(isChecked: Boolean) { override fun setNotificationPiggybackPreferenceChecked(isChecked: Boolean) {
findPreference<SwitchPreferenceCompat>(getString(R.string.pref_key_notifications_piggyback))?.isChecked = findPreference<SwitchPreferenceCompat>(getString(R.string.pref_key_notifications_piggyback))?.isChecked =
isChecked isChecked

View File

@ -26,12 +26,13 @@ class NotificationsPresenter @Inject constructor(
with(view) { with(view) {
enableNotification( enableNotification(
preferencesRepository.notificationsEnableKey, notificationKey = preferencesRepository.notificationsEnableKey,
preferencesRepository.isServiceEnabled enable = preferencesRepository.isServiceEnabled
) )
initView(appInfo.isDebug) initView(appInfo.isDebug)
} }
checkNotificationsPermissionState()
checkNotificationPiggybackState() checkNotificationPiggybackState()
Timber.i("Settings notifications view was initialized") Timber.i("Settings notifications view was initialized")
@ -49,12 +50,17 @@ class NotificationsPresenter @Inject constructor(
view?.openNotificationExactAlarmSettings() view?.openNotificationExactAlarmSettings()
} }
} }
notificationsEnableKey -> {
if (isNotificationsEnable && view?.isNotificationPermissionGranted == false) {
view?.requestNotificationPermissions()
}
}
isDebugNotificationEnableKey -> { isDebugNotificationEnableKey -> {
chuckerCollector.showNotification = isDebugNotificationEnable chuckerCollector.showNotification = isDebugNotificationEnable
} }
isNotificationPiggybackEnabledKey -> { isNotificationPiggybackEnabledKey -> {
if (isNotificationPiggybackEnabled && view?.isNotificationPermissionGranted == false) { if (isNotificationPiggybackEnabled && view?.isNotificationPiggybackPermissionGranted == false) {
view?.openNotificationPermissionDialog() view?.openNotificationPiggyBackPermissionDialog()
} }
} }
} }
@ -70,9 +76,15 @@ class NotificationsPresenter @Inject constructor(
view?.openSystemSettings() view?.openSystemSettings()
} }
fun onNotificationsPermissionResult() {
view?.run {
setNotificationPreferencesChecked(isNotificationPermissionGranted)
}
}
fun onNotificationPiggybackPermissionResult() { fun onNotificationPiggybackPermissionResult() {
view?.run { view?.run {
setNotificationPiggybackPreferenceChecked(isNotificationPermissionGranted) setNotificationPiggybackPreferenceChecked(isNotificationPiggybackPermissionGranted)
} }
} }
@ -80,10 +92,18 @@ class NotificationsPresenter @Inject constructor(
view?.setUpcomingLessonsNotificationPreferenceChecked(timetableNotificationHelper.canScheduleExactAlarms()) view?.setUpcomingLessonsNotificationPreferenceChecked(timetableNotificationHelper.canScheduleExactAlarms())
} }
private fun checkNotificationsPermissionState() {
if (preferencesRepository.isNotificationsEnable) {
view?.run {
setNotificationPreferencesChecked(isNotificationPermissionGranted)
}
}
}
private fun checkNotificationPiggybackState() { private fun checkNotificationPiggybackState() {
if (preferencesRepository.isNotificationPiggybackEnabled) { if (preferencesRepository.isNotificationPiggybackEnabled) {
view?.run { view?.run {
setNotificationPiggybackPreferenceChecked(isNotificationPermissionGranted) setNotificationPiggybackPreferenceChecked(isNotificationPiggybackPermissionGranted)
} }
} }
} }

View File

@ -6,6 +6,8 @@ interface NotificationsView : BaseView {
val isNotificationPermissionGranted: Boolean val isNotificationPermissionGranted: Boolean
val isNotificationPiggybackPermissionGranted: Boolean
fun initView(showDebugNotificationSwitch: Boolean) fun initView(showDebugNotificationSwitch: Boolean)
fun showFixSyncDialog() fun showFixSyncDialog()
@ -14,10 +16,16 @@ interface NotificationsView : BaseView {
fun enableNotification(notificationKey: String, enable: Boolean) fun enableNotification(notificationKey: String, enable: Boolean)
fun openNotificationPermissionDialog() fun requestNotificationPermissions()
fun openNotificationsPermissionDialog()
fun openNotificationPiggyBackPermissionDialog()
fun openNotificationExactAlarmSettings() fun openNotificationExactAlarmSettings()
fun setNotificationPreferencesChecked(isChecked: Boolean)
fun setNotificationPiggybackPreferenceChecked(isChecked: Boolean) fun setNotificationPiggybackPreferenceChecked(isChecked: Boolean)
fun setUpcomingLessonsNotificationPreferenceChecked(isChecked: Boolean) fun setUpcomingLessonsNotificationPreferenceChecked(isChecked: Boolean)

View File

@ -8,6 +8,7 @@ import android.view.MenuInflater
import android.view.View import android.view.View
import android.widget.Toast import android.widget.Toast
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import androidx.core.os.bundleOf
import androidx.core.view.get import androidx.core.view.get
import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
@ -24,6 +25,8 @@ import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.capitalise import io.github.wulkanowy.utils.capitalise
import io.github.wulkanowy.utils.getThemeAttrColor import io.github.wulkanowy.utils.getThemeAttrColor
import io.github.wulkanowy.utils.nullableSerializable
import io.github.wulkanowy.utils.serializable
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
@ -38,7 +41,9 @@ class StudentInfoFragment :
lateinit var studentInfoAdapter: StudentInfoAdapter lateinit var studentInfoAdapter: StudentInfoAdapter
override val titleStringId: Int override val titleStringId: Int
get() = when (requireArguments().getSerializable(INFO_TYPE_ARGUMENT_KEY) as? StudentInfoView.Type) { get() = when (
requireArguments().nullableSerializable<StudentInfoView.Type>(INFO_TYPE_ARGUMENT_KEY)
) {
StudentInfoView.Type.PERSONAL -> R.string.account_personal_data StudentInfoView.Type.PERSONAL -> R.string.account_personal_data
StudentInfoView.Type.CONTACT -> R.string.account_contact StudentInfoView.Type.CONTACT -> R.string.account_contact
StudentInfoView.Type.ADDRESS -> R.string.account_address StudentInfoView.Type.ADDRESS -> R.string.account_address
@ -58,13 +63,14 @@ class StudentInfoFragment :
fun newInstance(type: StudentInfoView.Type, studentWithSemesters: StudentWithSemesters) = fun newInstance(type: StudentInfoView.Type, studentWithSemesters: StudentWithSemesters) =
StudentInfoFragment().apply { StudentInfoFragment().apply {
arguments = Bundle().apply { arguments = bundleOf(
putSerializable(INFO_TYPE_ARGUMENT_KEY, type) INFO_TYPE_ARGUMENT_KEY to type,
putSerializable(STUDENT_ARGUMENT_KEY, studentWithSemesters) STUDENT_ARGUMENT_KEY to studentWithSemesters
} )
} }
} }
@Suppress("DEPRECATION")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setHasOptionsMenu(true) setHasOptionsMenu(true)
@ -74,9 +80,9 @@ class StudentInfoFragment :
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
binding = FragmentStudentInfoBinding.bind(view) binding = FragmentStudentInfoBinding.bind(view)
presenter.onAttachView( presenter.onAttachView(
this, view = this,
requireArguments().getSerializable(INFO_TYPE_ARGUMENT_KEY) as StudentInfoView.Type, type = requireArguments().serializable(INFO_TYPE_ARGUMENT_KEY),
requireArguments().getSerializable(STUDENT_ARGUMENT_KEY) as StudentWithSemesters studentWithSemesters = requireArguments().serializable(STUDENT_ARGUMENT_KEY),
) )
} }
@ -153,7 +159,6 @@ class StudentInfoFragment :
) )
} }
@OptIn(ExperimentalStdlibApi::class)
override fun showFamilyTypeData(studentInfo: StudentInfo) { override fun showFamilyTypeData(studentInfo: StudentInfo) {
val items = buildList { val items = buildList {
add(studentInfo.firstGuardian?.let { add(studentInfo.firstGuardian?.let {

View File

@ -8,14 +8,12 @@ import android.view.View
import android.view.View.GONE import android.view.View.GONE
import android.view.View.VISIBLE import android.view.View.VISIBLE
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Timetable import io.github.wulkanowy.data.db.entities.Timetable
import io.github.wulkanowy.databinding.DialogTimetableBinding import io.github.wulkanowy.databinding.DialogTimetableBinding
import io.github.wulkanowy.utils.capitalise import io.github.wulkanowy.utils.*
import io.github.wulkanowy.utils.getThemeAttrColor
import io.github.wulkanowy.utils.lifecycleAwareVariable
import io.github.wulkanowy.utils.toFormattedString
import java.time.Instant import java.time.Instant
class TimetableDialog : DialogFragment() { class TimetableDialog : DialogFragment() {
@ -28,17 +26,15 @@ class TimetableDialog : DialogFragment() {
private const val ARGUMENT_KEY = "Item" private const val ARGUMENT_KEY = "Item"
fun newInstance(exam: Timetable) = TimetableDialog().apply { fun newInstance(lesson: Timetable) = TimetableDialog().apply {
arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, exam) } arguments = bundleOf(ARGUMENT_KEY to lesson)
} }
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setStyle(STYLE_NO_TITLE, 0) setStyle(STYLE_NO_TITLE, 0)
arguments?.run { lesson = requireArguments().serializable(ARGUMENT_KEY)
lesson = getSerializable(ARGUMENT_KEY) as Timetable
}
} }
override fun onCreateView( override fun onCreateView(

View File

@ -7,6 +7,7 @@ import android.view.MenuItem
import android.view.View import android.view.View
import android.view.View.GONE import android.view.View.GONE
import android.view.View.VISIBLE import android.view.View.VISIBLE
import androidx.core.os.bundleOf
import androidx.core.text.parseAsHtml import androidx.core.text.parseAsHtml
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
@ -39,9 +40,7 @@ class TimetableFragment : BaseFragment<FragmentTimetableBinding>(R.layout.fragme
private const val ARGUMENT_DATE_KEY = "ARGUMENT_DATE" private const val ARGUMENT_DATE_KEY = "ARGUMENT_DATE"
fun newInstance(date: LocalDate? = null) = TimetableFragment().apply { fun newInstance(date: LocalDate? = null) = TimetableFragment().apply {
arguments = Bundle().apply { arguments = date?.let { bundleOf(ARGUMENT_DATE_KEY to it.toEpochDay()) }
date?.let { putLong(ARGUMENT_DATE_KEY, it.toEpochDay()) }
}
} }
} }
@ -51,6 +50,7 @@ class TimetableFragment : BaseFragment<FragmentTimetableBinding>(R.layout.fragme
override val currentStackSize get() = (activity as? MainActivity)?.currentStackSize override val currentStackSize get() = (activity as? MainActivity)?.currentStackSize
@Suppress("DEPRECATION")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setHasOptionsMenu(true) setHasOptionsMenu(true)

View File

@ -4,10 +4,12 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import io.github.wulkanowy.data.db.entities.CompletedLesson import io.github.wulkanowy.data.db.entities.CompletedLesson
import io.github.wulkanowy.databinding.DialogLessonCompletedBinding import io.github.wulkanowy.databinding.DialogLessonCompletedBinding
import io.github.wulkanowy.utils.lifecycleAwareVariable import io.github.wulkanowy.utils.lifecycleAwareVariable
import io.github.wulkanowy.utils.serializable
class CompletedLessonDialog : DialogFragment() { class CompletedLessonDialog : DialogFragment() {
@ -19,17 +21,15 @@ class CompletedLessonDialog : DialogFragment() {
private const val ARGUMENT_KEY = "Item" private const val ARGUMENT_KEY = "Item"
fun newInstance(exam: CompletedLesson) = CompletedLessonDialog().apply { fun newInstance(lesson: CompletedLesson) = CompletedLessonDialog().apply {
arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, exam) } arguments = bundleOf(ARGUMENT_KEY to lesson)
} }
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setStyle(STYLE_NO_TITLE, 0) setStyle(STYLE_NO_TITLE, 0)
arguments?.run { completedLesson = requireArguments().serializable(ARGUMENT_KEY)
completedLesson = getSerializable(ARGUMENT_KEY) as CompletedLesson
}
} }
override fun onCreateView( override fun onCreateView(

View File

@ -0,0 +1,9 @@
package io.github.wulkanowy.utils
abstract class BaseRemoteConfigHelper {
open fun initialize() = Unit
open val userAgentTemplate: String
get() = RemoteConfigDefaults.USER_AGENT_TEMPLATE.value as String
}

View File

@ -0,0 +1,32 @@
package io.github.wulkanowy.utils
import android.content.Intent
import android.os.Build
import android.os.Bundle
import java.io.Serializable
inline fun <reified T : Serializable> Bundle.serializable(key: String): T = when {
Build.VERSION.SDK_INT >= 33 -> getSerializable(key, T::class.java)!!
else -> @Suppress("DEPRECATION") getSerializable(key) as T
}
inline fun <reified T : Serializable> Bundle.nullableSerializable(key: String): T? = when {
Build.VERSION.SDK_INT >= 33 -> getSerializable(key, T::class.java)
else -> @Suppress("DEPRECATION") getSerializable(key) as T?
}
@Suppress("DEPRECATION", "UNCHECKED_CAST")
inline fun <reified T : Serializable> Bundle.parcelableArray(key: String): Array<T>? = when {
Build.VERSION.SDK_INT >= 33 -> getParcelableArray(key, T::class.java)
else -> getParcelableArray(key) as Array<T>?
}
inline fun <reified T : Serializable> Intent.serializable(key: String): T = when {
Build.VERSION.SDK_INT >= 33 -> getSerializableExtra(key, T::class.java)!!
else -> @Suppress("DEPRECATION") getSerializableExtra(key) as T
}
inline fun <reified T : Serializable> Intent.nullableSerializable(key: String): T? = when {
Build.VERSION.SDK_INT >= 33 -> getSerializableExtra(key, T::class.java)
else -> @Suppress("DEPRECATION") getSerializableExtra(key) as T?
}

View File

@ -1,11 +1,15 @@
package io.github.wulkanowy.utils package io.github.wulkanowy.utils
import android.app.Activity
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Build
import android.provider.CalendarContract import android.provider.CalendarContract
import android.provider.Settings
import io.github.wulkanowy.BuildConfig import io.github.wulkanowy.BuildConfig
import timber.log.Timber
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.ZoneId import java.time.ZoneId
@ -86,6 +90,23 @@ fun Context.openDialer(phone: String) {
} }
} }
fun Activity.openNotificationSettings() {
val intent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply {
putExtra("android.provider.extra.APP_PACKAGE", packageName)
}
} else {
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.fromParts("package", packageName, null)
}
}
try {
startActivity(intent)
} catch (e: Exception) {
Timber.e(e)
}
}
fun Context.shareText(text: String, subject: String?) { fun Context.shareText(text: String, subject: String?) {
val sendIntent: Intent = Intent().apply { val sendIntent: Intent = Intent().apply {
action = Intent.ACTION_SEND action = Intent.ACTION_SEND

View File

@ -0,0 +1,8 @@
package io.github.wulkanowy.utils
enum class RemoteConfigDefaults(val value: Any) {
USER_AGENT_TEMPLATE(""),
;
val key get() = name.lowercase()
}

View File

@ -1,7 +1,15 @@
package io.github.wulkanowy.utils package io.github.wulkanowy.utils
import androidx.core.text.parseAsHtml
import org.apache.commons.text.StringEscapeUtils
inline fun String?.ifNullOrBlank(defaultValue: () -> String) = inline fun String?.ifNullOrBlank(defaultValue: () -> String) =
if (isNullOrBlank()) defaultValue() else this if (isNullOrBlank()) defaultValue() else this
fun String.capitalise() = fun String.capitalise() =
replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() } replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() }
fun String.parseUonetHtml() = this
.let(StringEscapeUtils::unescapeHtml4)
.replace("\n", "<br/>")
.parseAsHtml()

View File

@ -1,8 +1,7 @@
Wersja 1.8.3 Wersja 1.9.2
- naprawiliśmy logowanie dla użytkowników systemu Resman Rzeszów - naprawiliśmy oznaczanie wiadomości jako odczytanych (problem dotyczył głównie kont rodziców z wieloma dziećmi w tej samej szkole)
- dodaliśmy wsparcie dla nowej platformy z Tomaszowa Mazowieckiego - naprawiliśmy zapisywanie załączników do wiadomości w sytuacji, gdy ten sam załącznik był dodany do więcej niż jednej wiadomości
- poprawiliśmy dopasowywanie skrzynek pocztowych do uczniów - usprawniliśmy ekran z wyborem uczniów i wpisywaniem symbolu przy pierwszym logowaniu
- naprawiliśmy literówkę w tytule wiadomości z szablonem usprawiedliwienia
Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases

View File

@ -0,0 +1,20 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="1926"
android:viewportHeight="1926">
<path
android:fillColor="#fff"
android:pathData="M920,1357.7v-184.5h42.7q19.3,0.3 34.3,6.6 15.2,6.2 25.6,17.5 10.6,11.2 16,26.9 5.6,15.6 5.7, 34.6v13.5q-0.1,19 -5.7,34.7 -5.4,15.6 -16,26.9 -10.4,11.2 -25.6,17.5 -15,6.2 -34.3,6.3zM943.8,1192.5v146.1h18.9q15, -0.1 25.8,-5.2 11,-5.2 18,-14 7.3,-9 10.7,-21 3.5,-12 3.6,-26v-13.9q0,-14 -3.6,-26t-10.7,-20.7q-7,-9 -18,-14 -10.9,-5 -25.8,-5.3zM1179.5,1272.5h-77v65.4h89.8v19.9L1079,1357.8v-184.6h112.2v20h-88.7v59.4h77zM1288.9, 1320l43.6,-146.8h25l-58.2,184.6h-20.4l-58.3,-184.6h25z"
android:strokeWidth="1"
android:strokeColor="#000" />
<clip-path android:pathData="M 0 0 L 1926 0 L 1926 1111 L 850 1111 L 850 1926 L 0 1926 z" />
<path
android:fillColor="#fff"
android:pathData="M1082,1260v4l-6,73c0,5 -5,9 -10,9H879c-4,0 -8,-2 -10,-5l-31,-58 -29,-71v-8l48,-94c2,-3 2,-5 1, -8l-24,-65c-1,-2 0,-5 1,-7l39,-84c4,-8 17,-8 20,1l12,38 38,101c1,2 3,5 6,6l80,27c3,1 6,3 7,6l45,135zM852,717c0,-23 15,-43 35,-52l-2,-8c0,-11 13,-20 29,-20h1a38,38 0,0 1,36 -18l3,-2c11,-16 37,-27 68,-27 12,0 23,2 32,4 3,-3 8,-6 14,-6s12,3 14,8c8,-9 22,-16 38,-16 25,0 45,16 45,36l-1,7 3,4c16,6 27,17 27,30 0,14 -15,27 -34,32 -2,0 -3,1 -3,3 0,11 -11,21 -26,22v1c0,22 -41,40 -92,40 -11,0 -21,-1 -31,-3v1c0,9 -12,16 -26,16h-2l2,7c0,14 -18,25 -40,25h-7l-4, 2c-2,7 -8,12 -15,12 -9,0 -16,-7 -16,-16 0,-5 1,-8 4,-11l1,-4c-2,-3 -2,-5 -2,-8 0,-2 -2,-3 -3,-3 -27,-6 -48,-29 -48,-56z" />
<path
android:fillColor="#fff"
android:pathData="M1336,1346h-125c-3,0 -6,-1 -8,-4l-84,-118 -1,-2 -42,-122 -5,-5 -102,-52 -5,-6 -15,-75 -2,-3 -34,-51c-2,-3 -2,-6 -1,-9l21,-45c2,-4 5,-6 9,-6l39,-2c3,0 5,-1 6,-3l29,-22c5,-4 14,-2 17,4l98,201 1,2 13,81 2,5 197,216c6,6 1,16 -8,16zM801,1302c1,3 1,6 -1,8l-19,31c-2,3 -5,5 -9,5H591c-8,0 -13,-8 -9,-15l116,-171 2,-3 53, -228c1,-3 3,-5 5,-6l72,-40c3,-1 5,-4 5,-7l10,-42c2,-9 15,-11 20,-3l3,6c2,2 2,5 1,8l-66,190v5l16,91v5l-40,88v6l22,72z" />
</vector>

View File

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="1926"
android:viewportHeight="1926">
<path
android:fillColor="#fff"
android:pathData="M1082,1260v4l-6,73c0,5 -5,9 -10,9H879c-4,0 -8,-2 -10,-5l-31,-58 -29,-71v-8l48,-94c2,-3 2,-5 1, -8l-24,-65c-1,-2 0,-5 1,-7l39,-84c4,-8 17,-8 20,1l12,38 38,101c1,2 3,5 6,6l80,27c3,1 6,3 7,6l45,135zM852,717c0,-23 15,-43 35,-52l-2,-8c0,-11 13,-20 29,-20h1a38,38 0,0 1,36 -18l3,-2c11,-16 37,-27 68,-27 12,0 23,2 32,4 3,-3 8,-6 14,-6s12,3 14,8c8,-9 22,-16 38,-16 25,0 45,16 45,36l-1,7 3,4c16,6 27,17 27,30 0,14 -15,27 -34,32 -2,0 -3,1 -3,3 0,11 -11,21 -26,22v1c0,22 -41,40 -92,40 -11,0 -21,-1 -31,-3v1c0,9 -12,16 -26,16h-2l2,7c0,14 -18,25 -40,25h-7l-4, 2c-2,7 -8,12 -15,12 -9,0 -16,-7 -16,-16 0,-5 1,-8 4,-11l1,-4c-2,-3 -2,-5 -2,-8 0,-2 -2,-3 -3,-3 -27,-6 -48,-29 -48,-56z" />
<path
android:fillColor="#fff"
android:pathData="M1336,1346h-125c-3,0 -6,-1 -8,-4l-84,-118 -1,-2 -42,-122 -5,-5 -102,-52 -5,-6 -15,-75 -2,-3 -34,-51c-2,-3 -2,-6 -1,-9l21,-45c2,-4 5,-6 9,-6l39,-2c3,0 5,-1 6,-3l29,-22c5,-4 14,-2 17,4l98,201 1,2 13,81 2,5 197,216c6,6 1,16 -8,16zM801,1302c1,3 1,6 -1,8l-19,31c-2,3 -5,5 -9,5H591c-8,0 -13,-8 -9,-15l116,-171 2,-3 53, -228c1,-3 3,-5 5,-6l72,-40c3,-1 5,-4 5,-7l10,-42c2,-9 15,-11 20,-3l3,6c2,2 2,5 1,8l-66,190v5l16,91v5l-40,88v6l22,72z" />
</vector>

View File

@ -40,7 +40,7 @@
android:layout_marginStart="0dp" android:layout_marginStart="0dp"
android:layout_marginTop="28dp" android:layout_marginTop="28dp"
android:layout_marginEnd="24dp" android:layout_marginEnd="24dp"
android:text="@string/all_title" android:text="@string/conference_place"
android:textColor="?android:textColorSecondary" android:textColor="?android:textColorSecondary"
android:textSize="12sp" android:textSize="12sp"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
@ -71,7 +71,7 @@
android:layout_marginStart="0dp" android:layout_marginStart="0dp"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:layout_marginEnd="24dp" android:layout_marginEnd="24dp"
android:text="@string/all_subject" android:text="@string/conference_topic"
android:textColor="?android:textColorSecondary" android:textColor="?android:textColorSecondary"
android:textSize="12sp" android:textSize="12sp"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"

View File

@ -111,9 +111,11 @@
<TextView <TextView
android:id="@+id/accountDetailsName" android:id="@+id/accountDetailsName"
android:layout_width="wrap_content" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:gravity="center_horizontal"
android:textSize="24sp" android:textSize="24sp"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
@ -122,9 +124,11 @@
<TextView <TextView
android:id="@+id/accountDetailsSchool" android:id="@+id/accountDetailsSchool"
android:layout_width="wrap_content" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="5dp" android:layout_marginTop="5dp"
android:gravity="center_horizontal"
android:textColor="?android:textColorSecondary" android:textColor="?android:textColorSecondary"
android:textSize="14sp" android:textSize="14sp"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"

View File

@ -3,92 +3,14 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical"> android:orientation="vertical"
tools:context=".ui.modules.login.studentselect.LoginStudentSelectFragment">
<com.google.android.material.progressindicator.CircularProgressIndicator
android:id="@+id/loginStudentSelectProgress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="true"
android:visibility="gone" />
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/loginStudentSelectContent" android:id="@+id/loginStudentSelectContent"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<LinearLayout
android:id="@+id/loginStudentSelectContact"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible">
<View
android:id="@+id/loginStudentSelectContactTopDivider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?android:attr/listDivider" />
<TextView
android:id="@+id/loginStudentSelectContactHeader"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginLeft="32dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="32dp"
android:layout_marginRight="32dp"
android:layout_marginBottom="16dp"
android:gravity="center_horizontal"
android:text="@string/login_contact_header"
android:textSize="14sp"
app:fontFamily="sans-serif-medium" />
<LinearLayout
android:id="@+id/loginStudentSelectContactButtons"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:orientation="horizontal">
<com.google.android.material.button.MaterialButton
android:id="@+id/loginStudentSelectContactEmail"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:layout_weight="1"
android:text="@string/login_contact_email"
app:icon="@drawable/ic_more_messages" />
<com.google.android.material.button.MaterialButton
android:id="@+id/loginStudentSelectContactDiscord"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_weight="1"
android:text="@string/login_contact_discord"
app:icon="@drawable/ic_about_discord" />
</LinearLayout>
<View
android:id="@+id/loginStudentSelectContactBottomDivider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="8dp"
android:background="?android:attr/listDivider" />
</LinearLayout>
<TextView <TextView
android:id="@+id/loginStudentSelectHeader" android:id="@+id/loginStudentSelectHeader"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -106,22 +28,24 @@
app:layout_constraintBottom_toTopOf="@id/loginStudentSelectRecycler" app:layout_constraintBottom_toTopOf="@id/loginStudentSelectRecycler"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/loginStudentSelectContact" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed" /> app:layout_constraintVertical_chainStyle="packed" />
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/loginStudentSelectRecycler" android:id="@+id/loginStudentSelectRecycler"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:minHeight="144dp" android:fadingEdge="vertical"
android:fadingEdgeLength="100dp"
android:requiresFadingEdge="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constrainedHeight="true" app:layout_constrainedHeight="true"
app:layout_constraintBottom_toTopOf="@id/loginStudentSelectSignIn" app:layout_constraintBottom_toTopOf="@id/loginStudentSelectSignIn"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_max="432dp"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/loginStudentSelectHeader" app:layout_constraintTop_toBottomOf="@id/loginStudentSelectHeader"
tools:itemCount="6" tools:itemCount="33"
tools:listitem="@layout/item_login_student_select" /> tools:listitem="@layout/item_login_student_select_student" />
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/loginStudentSelectSignIn" android:id="@+id/loginStudentSelectSignIn"
@ -136,4 +60,12 @@
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/loginStudentSelectRecycler" /> app:layout_constraintTop_toBottomOf="@id/loginStudentSelectRecycler" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.progressindicator.CircularProgressIndicator
android:id="@+id/loginStudentSelectProgress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="true"
android:visibility="gone" />
</FrameLayout> </FrameLayout>

View File

@ -0,0 +1,69 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.modules.notifications.NotificationsFragment">
<ImageView
android:id="@+id/notifications_header_icon"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_marginTop="32dp"
android:src="@drawable/ic_settings_notifications"
app:layout_constraintBottom_toTopOf="@id/notifications_header_title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.4"
app:layout_constraintVertical_chainStyle="packed"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/notifications_header_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
android:layout_marginVertical="16dp"
android:gravity="center_horizontal"
android:text="@string/notifications_header_title"
android:textColor="?android:textColorPrimary"
android:textSize="24sp"
app:layout_constraintBottom_toTopOf="@id/notifications_header_description"
app:layout_constraintTop_toBottomOf="@id/notifications_header_icon"
app:lineHeight="30sp" />
<TextView
android:id="@+id/notifications_header_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
android:gravity="center_horizontal"
android:text="@string/notifications_header_description"
android:textColor="?android:textColorSecondary"
android:textSize="16sp"
app:layout_constraintBottom_toTopOf="@id/notifications_skip"
app:layout_constraintTop_toBottomOf="@id/notifications_header_title"
app:lineHeight="24sp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/notifications_skip"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:text="@string/notifications_skip"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/notifications_enable"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:text="@string/notifications_enable"
app:icon="@drawable/ic_settings_notifications"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -26,27 +26,31 @@
<TextView <TextView
android:id="@+id/dashboard_account_item_name" android:id="@+id/dashboard_account_item_name"
android:layout_width="wrap_content" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:textSize="16sp" android:textSize="16sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/dashboard_account_item_avatar" app:layout_constraintStart_toEndOf="@id/dashboard_account_item_avatar"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:text="John Smith" /> tools:text="John Smith" />
<TextView <TextView
android:id="@+id/dashboard_account_item_school_name" android:id="@+id/dashboard_account_item_school_name"
android:layout_width="wrap_content" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp" android:layout_marginBottom="16dp"
android:textColor="?android:textColorSecondary" android:textColor="?android:textColorSecondary"
android:textSize="12sp" android:textSize="12sp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/dashboard_account_item_avatar" app:layout_constraintStart_toEndOf="@id/dashboard_account_item_avatar"
app:layout_constraintTop_toBottomOf="@id/dashboard_account_item_name" app:layout_constraintTop_toBottomOf="@id/dashboard_account_item_name"
tools:text="Szkoła Wulkanowego " /> tools:text="Szkoła Wulkanowego" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.progressindicator.CircularProgressIndicator <com.google.android.material.progressindicator.CircularProgressIndicator
@ -56,4 +60,4 @@
android:layout_gravity="center" android:layout_gravity="center"
android:indeterminate="true" android:indeterminate="true"
android:visibility="gone" /> android:visibility="gone" />
</com.google.android.material.card.MaterialCardView> </com.google.android.material.card.MaterialCardView>

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:background="?selectableItemBackground"
android:paddingHorizontal="16dp">
<com.google.android.material.divider.MaterialDivider
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/login_student_select_empty_symbol_header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingVertical="16dp"
android:text="@string/login_other_search_locations"
android:textColor="?android:textColorPrimary"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/login_student_select_empty_symbol_chevron"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginEnd="16dp"
android:rotation="90"
android:src="@drawable/ic_chevron_right"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:tint="?android:textColorPrimary"
tools:ignore="ContentDescription" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="24dp"
android:paddingVertical="8dp">
<TextView
android:id="@+id/login_student_select_header_school_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="?android:textColorPrimary"
app:layout_constraintTop_toTopOf="parent"
tools:text="Publiczna szkoła Wulkanowego" />
<TextView
android:id="@+id/login_student_select_header_school_details"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:text="@string/login_no_active_student"
android:textColor="?colorTimetableChange"
app:layout_constraintTop_toBottomOf="@id/login_student_select_header_school_name" />
<TextView
android:id="@+id/login_student_select_header_school_error"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:ellipsize="end"
android:maxLines="2"
android:textColor="?colorError"
app:layout_constraintTop_toBottomOf="@id/login_student_select_header_school_details"
tools:text="@tools:sample/lorem/random" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
android:paddingVertical="8dp">
<TextView
android:id="@+id/login_student_select_header_symbol_value"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="?android:textColorPrimary"
app:layout_constraintTop_toTopOf="parent"
tools:text="powiatjaroslawski" />
<TextView
android:id="@+id/login_student_select_header_symbol_username"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:textColor="?android:textColorSecondary"
app:layout_constraintTop_toBottomOf="@id/login_student_select_header_symbol_value"
tools:text="Jan Kowalski" />
<TextView
android:id="@+id/login_student_select_header_symbol_error"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:ellipsize="end"
android:maxLines="2"
android:textColor="?colorError"
app:layout_constraintTop_toBottomOf="@id/login_student_select_header_symbol_username"
tools:text="@tools:sample/lorem/random" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
android:paddingVertical="8dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/login_student_select_help_symbol"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/login_symbol_enter"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
<TextView
android:id="@+id/login_student_select_help_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/login_contact_header"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/login_student_select_help_mail"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/login_student_select_help_symbol" />
<com.google.android.material.button.MaterialButton
android:id="@+id/login_student_select_help_mail"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:contentDescription="@string/login_contact_email"
app:icon="@drawable/ic_more_messages"
app:iconGravity="textStart"
app:iconPadding="0dp"
app:layout_constraintEnd_toEndOf="@id/login_student_select_help_title"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toEndOf="@id/login_student_select_help_title"
app:layout_constraintTop_toBottomOf="@id/login_student_select_help_symbol" />
<com.google.android.material.button.MaterialButton
android:id="@+id/login_student_select_help_discord"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:contentDescription="@string/login_contact_discord"
app:icon="@drawable/ic_about_discord"
app:iconGravity="textStart"
app:iconPadding="0dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/login_student_select_help_mail"
app:layout_constraintTop_toTopOf="@id/login_student_select_help_mail" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -4,7 +4,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?selectableItemBackground" android:background="?selectableItemBackground"
android:minHeight="72dp" android:minHeight="56dp"
android:paddingTop="8dp" android:paddingTop="8dp"
android:paddingBottom="8dp" android:paddingBottom="8dp"
tools:context=".ui.modules.login.studentselect.LoginStudentSelectAdapter"> tools:context=".ui.modules.login.studentselect.LoginStudentSelectAdapter">
@ -14,9 +14,10 @@
android:layout_width="32dp" android:layout_width="32dp"
android:layout_height="24dp" android:layout_height="24dp"
android:layout_centerVertical="true" android:layout_centerVertical="true"
android:layout_marginStart="12dp" android:layout_marginStart="32dp"
android:layout_marginEnd="28dp" android:layout_marginEnd="28dp"
android:background="@android:color/transparent" android:background="@android:color/transparent"
android:clickable="false"
tools:text=" " /> tools:text=" " />
<TextView <TextView
@ -32,34 +33,18 @@
tools:text="@tools:sample/full_names" /> tools:text="@tools:sample/full_names" />
<TextView <TextView
android:id="@+id/loginItemSchool" android:id="@+id/loginItemSignedIn"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_below="@id/loginItemName" android:layout_below="@id/loginItemName"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:layout_toEndOf="@id/loginItemCheck" android:layout_toEndOf="@id/loginItemCheck"
android:ellipsize="end" android:ellipsize="end"
android:gravity="bottom"
android:maxLines="2"
android:textColor="?android:textColorSecondary"
android:textSize="14sp"
tools:text="@tools:sample/lorem/random" />
<TextView
android:id="@+id/loginItemSignedIn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minHeight="20dp"
android:layout_below="@id/loginItemSchool"
android:layout_marginEnd="16dp"
android:layout_toEndOf="@id/loginItemCheck"
android:ellipsize="end"
android:enabled="false" android:enabled="false"
android:gravity="bottom" android:gravity="bottom"
android:maxLines="1" android:maxLines="1"
android:minHeight="20dp"
android:text="@string/login_signed_in" android:text="@string/login_signed_in"
android:textColor="?android:textColorSecondary" android:textColor="?android:textColorSecondary"
android:textSize="14sp" android:textSize="14sp" />
android:visibility="gone"
tools:visibility="visible" />
</RelativeLayout> </RelativeLayout>

View File

@ -2,4 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/colorPrimary" /> <background android:drawable="@color/colorPrimary" />
<foreground android:drawable="@drawable/ic_launcher_foreground" /> <foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon> <monochrome android:drawable="@drawable/ic_launcher_foreground_mono" />
</adaptive-icon>

View File

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/colorPrimary" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

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