1
0
mirror of https://github.com/wulkanowy/wulkanowy.git synced 2024-09-20 10:19:09 -05:00

Merge branch 'release/2.0.0'

This commit is contained in:
Mikołaj Pich 2023-05-07 23:40:25 +02:00
commit 18dbbba328
282 changed files with 4036 additions and 3306 deletions

1
.gitignore vendored
View File

@ -119,3 +119,4 @@ Thumbs.db
app/src/release/agconnect-services.json app/src/release/agconnect-services.json
app/src/release/agconnect-credentials.json app/src/release/agconnect-credentials.json
.idea/deploymentTargetDropDown.xml .idea/deploymentTargetDropDown.xml
.idea/kotlinc.xml

View File

@ -23,8 +23,8 @@ android {
testApplicationId "io.github.tests.wulkanowy" testApplicationId "io.github.tests.wulkanowy"
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 33 targetSdkVersion 33
versionCode 121 versionCode 122
versionName "1.9.2" versionName "2.0.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resValue "string", "app_name", "Wulkanowy" resValue "string", "app_name", "Wulkanowy"
@ -177,36 +177,36 @@ huaweiPublish {
} }
ext { ext {
work_manager = "2.7.1" work_manager = "2.8.1"
android_hilt = "1.0.0" android_hilt = "1.0.0"
room = "2.4.3" room = "2.5.1"
chucker = "3.5.2" chucker = "3.5.2"
mockk = "1.13.3" mockk = "1.13.5"
coroutines = "1.6.4" coroutines = "1.6.4"
} }
dependencies { dependencies {
implementation "io.github.wulkanowy:sdk:1.9.2" implementation "io.github.wulkanowy:sdk:2.0.0"
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.8' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3'
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1" implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines"
implementation "androidx.core:core-ktx:1.9.0" implementation "androidx.core:core-ktx:1.10.0"
implementation 'androidx.core:core-splashscreen:1.0.0' implementation 'androidx.core:core-splashscreen:1.0.1'
implementation "androidx.activity:activity-ktx:1.6.1" implementation "androidx.activity:activity-ktx:1.7.1"
implementation "androidx.appcompat:appcompat:1.5.1" implementation "androidx.appcompat:appcompat:1.6.1"
implementation "androidx.fragment:fragment-ktx:1.5.5" implementation "androidx.fragment:fragment-ktx:1.5.7"
implementation "androidx.annotation:annotation:1.5.0" implementation "androidx.annotation:annotation:1.6.0"
implementation "androidx.preference:preference-ktx:1.2.0" implementation "androidx.preference:preference-ktx:1.2.0"
implementation "androidx.recyclerview:recyclerview:1.2.1" implementation "androidx.recyclerview:recyclerview:1.3.0"
implementation "androidx.viewpager2:viewpager2:1.1.0-beta01" implementation "androidx.viewpager2:viewpager2:1.1.0-beta01"
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
implementation "androidx.constraintlayout:constraintlayout:2.1.4" implementation "androidx.constraintlayout:constraintlayout:2.1.4"
implementation "androidx.coordinatorlayout:coordinatorlayout:1.2.0" implementation "androidx.coordinatorlayout:coordinatorlayout:1.2.0"
implementation "com.google.android.material:material:1.7.0" implementation "com.google.android.material:material:1.8.0"
implementation "com.github.wulkanowy:material-chips-input:2.3.1" implementation "com.github.wulkanowy:material-chips-input:2.3.1"
implementation "com.github.PhilJay:MPAndroidChart:v3.1.0" implementation "com.github.PhilJay:MPAndroidChart:v3.1.0"
implementation 'com.github.lopspower:CircularImageView:4.3.0' implementation 'com.github.lopspower:CircularImageView:4.3.0'
@ -214,7 +214,7 @@ dependencies {
implementation "androidx.work:work-runtime-ktx:$work_manager" implementation "androidx.work:work-runtime-ktx:$work_manager"
playImplementation "androidx.work:work-gcm:$work_manager" playImplementation "androidx.work:work-gcm:$work_manager"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.5.1" implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.6.1"
implementation "androidx.room:room-runtime:$room" implementation "androidx.room:room-runtime:$room"
implementation "androidx.room:room-ktx:$room" implementation "androidx.room:room-ktx:$room"
@ -229,30 +229,30 @@ dependencies {
implementation "com.github.YarikSOffice:lingver:1.3.0" implementation "com.github.YarikSOffice:lingver:1.3.0"
implementation 'com.squareup.retrofit2:retrofit:2.9.0' implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:0.8.0" implementation "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0"
implementation "com.squareup.okhttp3:logging-interceptor:4.10.0" implementation "com.squareup.okhttp3:logging-interceptor:4.11.0"
implementation "com.jakewharton.timber:timber:5.0.1" implementation "com.jakewharton.timber:timber:5.0.1"
implementation "at.favre.lib:slf4j-timber:1.0.1" implementation "at.favre.lib:slf4j-timber:1.0.1"
implementation 'com.github.bastienpaulfr:Treessence:1.0.5' implementation 'com.github.bastienpaulfr:Treessence:1.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.3.0"
implementation "io.github.wulkanowy:AppKillerManager:3.0.1" implementation "io.github.wulkanowy:AppKillerManager:3.0.1"
implementation 'me.xdrop:fuzzywuzzy:1.4.0' implementation 'me.xdrop:fuzzywuzzy:1.4.0'
implementation 'com.fredporciuncula:flow-preferences:1.8.0' implementation 'com.fredporciuncula:flow-preferences:1.9.1'
implementation 'org.apache.commons:commons-text:1.10.0' implementation 'org.apache.commons:commons-text:1.10.0'
playImplementation platform('com.google.firebase:firebase-bom:31.1.1') playImplementation platform('com.google.firebase:firebase-bom:31.5.0')
playImplementation 'com.google.firebase:firebase-analytics-ktx' playImplementation 'com.google.firebase:firebase-analytics-ktx'
playImplementation 'com.google.firebase:firebase-messaging:' playImplementation 'com.google.firebase:firebase-messaging:'
playImplementation 'com.google.firebase:firebase-crashlytics:' playImplementation 'com.google.firebase:firebase-crashlytics:'
playImplementation 'com.google.firebase:firebase-config-ktx' playImplementation 'com.google.firebase:firebase-config-ktx'
playImplementation 'com.google.android.play:core:1.10.3' playImplementation 'com.google.android.play:core:1.10.3'
playImplementation 'com.google.android.play:core-ktx:1.8.1' playImplementation 'com.google.android.play:core-ktx:1.8.1'
playImplementation 'com.google.android.gms:play-services-ads:21.4.0' playImplementation 'com.google.android.gms:play-services-ads:22.0.0'
hmsImplementation 'com.huawei.hms:hianalytics:6.9.0.301' hmsImplementation 'com.huawei.hms:hianalytics:6.9.1.200'
hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.7.3.302' hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.9.0.300'
releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker" releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker"
@ -265,17 +265,17 @@ 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.2' testImplementation 'org.robolectric:robolectric:4.10'
testImplementation "androidx.test:runner:1.5.1" testImplementation "androidx.test:runner:1.5.2"
testImplementation "androidx.test.ext:junit:1.1.4" testImplementation "androidx.test.ext:junit:1.1.5"
testImplementation "androidx.test:core:1.5.0" testImplementation "androidx.test:core:1.5.0"
testImplementation "androidx.room:room-testing:$room" testImplementation "androidx.room:room-testing:$room"
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.5.0" androidTestImplementation "androidx.test:core:1.5.0"
androidTestImplementation "androidx.test:runner:1.5.1" androidTestImplementation "androidx.test:runner:1.5.2"
androidTestImplementation "androidx.test.ext:junit:1.1.4" androidTestImplementation "androidx.test.ext:junit:1.1.5"
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"
} }

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<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/colorIcon" />
<foreground android:drawable="@drawable/ic_launcher_foreground_dev" /> <foreground android:drawable="@drawable/ic_launcher_foreground_dev" />
<monochrome android:drawable="@drawable/ic_launcher_foreground_dev_mono" /> <monochrome android:drawable="@drawable/ic_launcher_foreground_dev_mono" />
</adaptive-icon> </adaptive-icon>

View File

@ -72,7 +72,7 @@
android:name=".ui.modules.message.send.SendMessageActivity" android:name=".ui.modules.message.send.SendMessageActivity"
android:configChanges="orientation|screenSize" android:configChanges="orientation|screenSize"
android:label="@string/send_message_title" android:label="@string/send_message_title"
android:theme="@style/WulkanowyTheme.MessageSend" android:theme="@style/WulkanowyTheme.NoActionBar"
android:windowSoftInputMode="adjustResize" /> android:windowSoftInputMode="adjustResize" />
<activity <activity
android:name=".ui.modules.timetablewidget.TimetableWidgetConfigureActivity" android:name=".ui.modules.timetablewidget.TimetableWidgetConfigureActivity"

View File

@ -20,7 +20,6 @@ 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 io.github.wulkanowy.utils.RemoteConfigHelper
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
@ -81,7 +80,6 @@ internal class DataModule {
.readTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS)
.build() .build()
@OptIn(ExperimentalSerializationApi::class)
@Singleton @Singleton
@Provides @Provides
fun provideRetrofit( fun provideRetrofit(

View File

@ -1,7 +1,6 @@
package io.github.wulkanowy.data.db.dao package io.github.wulkanowy.data.db.dao
import androidx.room.* import androidx.room.*
import androidx.room.OnConflictStrategy.ABORT
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentNickAndAvatar import io.github.wulkanowy.data.db.entities.StudentNickAndAvatar
import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.data.db.entities.StudentWithSemesters
@ -11,7 +10,7 @@ import javax.inject.Singleton
@Dao @Dao
abstract class StudentDao { abstract class StudentDao {
@Insert(onConflict = ABORT) @Insert(onConflict = OnConflictStrategy.ABORT)
abstract suspend fun insertAll(student: List<Student>): List<Long> abstract suspend fun insertAll(student: List<Student>): List<Long>
@Delete @Delete

View File

@ -22,6 +22,7 @@ data class Exam(
val subject: String, val subject: String,
@Deprecated("not available anymore")
val group: String, val group: String,
val type: String, val type: String,

View File

@ -10,9 +10,9 @@ fun List<SdkConference>.mapToEntities(semester: Semester) = map {
diaryId = semester.diaryId, diaryId = semester.diaryId,
agenda = it.agenda, agenda = it.agenda,
conferenceId = it.id, conferenceId = it.id,
date = it.dateZoned.toInstant(), date = it.date.toInstant(),
presentOnConference = it.presentOnConference, presentOnConference = it.presentOnConference,
subject = it.subject, subject = it.topic,
title = it.title title = it.place,
) )
} }

View File

@ -11,7 +11,7 @@ fun List<SdkExam>.mapToEntities(semester: Semester) = map {
date = it.date, date = it.date,
entryDate = it.entryDate, entryDate = it.entryDate,
subject = it.subject, subject = it.subject,
group = it.group, group = "",
type = it.type, type = it.type,
description = it.description, description = it.description,
teacher = it.teacher, teacher = it.teacher,

View File

@ -26,7 +26,7 @@ fun List<SdkMessage>.mapToEntities(
messageId = it.id, messageId = it.id,
correspondents = it.correspondents, correspondents = it.correspondents,
subject = it.subject.trim(), subject = it.subject.trim(),
date = it.dateZoned.toInstant(), date = it.date.toInstant(),
folderId = it.folderId, folderId = it.folderId,
unread = it.unread, unread = it.unread,
unreadBy = it.unreadBy, unreadBy = it.unreadBy,

View File

@ -9,7 +9,7 @@ import io.github.wulkanowy.sdk.pojo.Token as SdkToken
fun List<SdkDevice>.mapToEntities(student: Student) = map { fun List<SdkDevice>.mapToEntities(student: Student) = map {
MobileDevice( MobileDevice(
userLoginId = student.userLoginId, userLoginId = student.userLoginId,
date = it.createDateZoned.toInstant(), date = it.createDate.toInstant(),
deviceId = it.id, deviceId = it.id,
name = it.name name = it.name
) )

View File

@ -3,22 +3,24 @@ package io.github.wulkanowy.data.mappers
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.data.pojos.* import io.github.wulkanowy.data.pojos.*
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.sdk.mapper.mapSemesters
import java.time.Instant import java.time.Instant
import io.github.wulkanowy.sdk.scrapper.register.RegisterStudent as SdkRegisterStudent import io.github.wulkanowy.sdk.pojo.RegisterStudent as SdkRegisterStudent
import io.github.wulkanowy.sdk.scrapper.register.RegisterUser as SdkRegisterUser import io.github.wulkanowy.sdk.pojo.RegisterUser as SdkRegisterUser
fun SdkRegisterUser.mapToPojo(password: String) = RegisterUser( fun SdkRegisterUser.mapToPojo(password: String?) = RegisterUser(
email = email, email = email,
login = login, login = login,
password = password, password = password,
baseUrl = baseUrl, scrapperBaseUrl = scrapperBaseUrl,
loginMode = loginMode,
loginType = loginType, loginType = loginType,
symbols = symbols.map { registerSymbol -> symbols = symbols.map { registerSymbol ->
RegisterSymbol( RegisterSymbol(
symbol = registerSymbol.symbol, symbol = registerSymbol.symbol,
error = registerSymbol.error, error = registerSymbol.error,
hebeBaseUrl = registerSymbol.hebeBaseUrl,
keyId = registerSymbol.keyId,
privatePem = registerSymbol.privatePem,
userName = registerSymbol.userName, userName = registerSymbol.userName,
schools = registerSymbol.schools.map { schools = registerSymbol.schools.map {
RegisterUnit( RegisterUnit(
@ -42,14 +44,13 @@ fun SdkRegisterUser.mapToPojo(password: String) = RegisterUser(
classId = registerSubject.classId, classId = registerSubject.classId,
isParent = registerSubject.isParent, isParent = registerSubject.isParent,
semesters = registerSubject.semesters semesters = registerSubject.semesters
.mapSemesters()
.mapToEntities(registerSubject.studentId), .mapToEntities(registerSubject.studentId),
) )
}, },
) )
} }
) )
} },
) )
fun RegisterStudent.mapToStudentWithSemesters( fun RegisterStudent.mapToStudentWithSemesters(
@ -68,17 +69,17 @@ fun RegisterStudent.mapToStudentWithSemesters(
classId = classId, classId = classId,
studentId = studentId, studentId = studentId,
symbol = symbol.symbol, symbol = symbol.symbol,
loginType = user.loginType.name, loginType = user.loginType?.name.orEmpty(),
schoolName = unit.schoolName, schoolName = unit.schoolName,
schoolShortName = unit.schoolShortName, schoolShortName = unit.schoolShortName,
schoolSymbol = unit.schoolId, schoolSymbol = unit.schoolId,
studentName = "$studentName $studentSurname", studentName = "$studentName $studentSurname",
loginMode = Sdk.Mode.SCRAPPER.name, loginMode = user.loginMode.name,
scrapperBaseUrl = user.baseUrl, scrapperBaseUrl = user.scrapperBaseUrl.orEmpty(),
mobileBaseUrl = "", mobileBaseUrl = symbol.hebeBaseUrl.orEmpty(),
certificateKey = "", certificateKey = symbol.keyId.orEmpty(),
privateKey = "", privateKey = symbol.privatePem.orEmpty(),
password = user.password, password = user.password.orEmpty(),
isCurrent = false, isCurrent = false,
registrationDate = Instant.now(), registrationDate = Instant.now(),
).apply { ).apply {

View File

@ -1,37 +0,0 @@
package io.github.wulkanowy.data.mappers
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import java.time.Instant
import io.github.wulkanowy.sdk.pojo.Student as SdkStudent
fun List<SdkStudent>.mapToEntities(password: String = "", colors: List<Long>) = map {
StudentWithSemesters(
student = Student(
email = it.email,
password = password,
isParent = it.isParent,
symbol = it.symbol,
studentId = it.studentId,
userLoginId = it.userLoginId,
userName = it.userName,
studentName = it.studentName + " " + it.studentSurname,
schoolSymbol = it.schoolSymbol,
schoolShortName = it.schoolShortName,
schoolName = it.schoolName,
className = it.className,
classId = it.classId,
scrapperBaseUrl = it.scrapperBaseUrl,
loginType = it.loginType.name,
isCurrent = false,
registrationDate = Instant.now(),
mobileBaseUrl = it.mobileBaseUrl,
privateKey = it.privateKey,
certificateKey = it.certificateKey,
loginMode = it.loginMode.name,
).apply {
avatarColor = colors.random()
},
semesters = it.semesters.mapToEntities(it.studentId)
)
}

View File

@ -5,10 +5,10 @@ import io.github.wulkanowy.data.db.entities.Timetable
import io.github.wulkanowy.data.db.entities.TimetableAdditional import io.github.wulkanowy.data.db.entities.TimetableAdditional
import io.github.wulkanowy.data.db.entities.TimetableHeader import io.github.wulkanowy.data.db.entities.TimetableHeader
import io.github.wulkanowy.data.pojos.TimetableFull import io.github.wulkanowy.data.pojos.TimetableFull
import io.github.wulkanowy.sdk.pojo.TimetableFull as SdkTimetableFull import io.github.wulkanowy.sdk.pojo.Timetable as SdkTimetableFull
import io.github.wulkanowy.sdk.pojo.TimetableDayHeader as SdkTimetableHeader import io.github.wulkanowy.sdk.pojo.TimetableDayHeader as SdkTimetableHeader
import io.github.wulkanowy.sdk.pojo.Timetable as SdkTimetable import io.github.wulkanowy.sdk.pojo.Lesson as SdkLesson
import io.github.wulkanowy.sdk.pojo.TimetableAdditional as SdkTimetableAdditional import io.github.wulkanowy.sdk.pojo.LessonAdditional as SdkTimetableAdditional
fun SdkTimetableFull.mapToEntities(semester: Semester) = TimetableFull( fun SdkTimetableFull.mapToEntities(semester: Semester) = TimetableFull(
lessons = lessons.mapToEntities(semester), lessons = lessons.mapToEntities(semester),
@ -16,13 +16,13 @@ fun SdkTimetableFull.mapToEntities(semester: Semester) = TimetableFull(
headers = headers.mapToEntities(semester) headers = headers.mapToEntities(semester)
) )
fun List<SdkTimetable>.mapToEntities(semester: Semester) = map { fun List<SdkLesson>.mapToEntities(semester: Semester) = map {
Timetable( Timetable(
studentId = semester.studentId, studentId = semester.studentId,
diaryId = semester.diaryId, diaryId = semester.diaryId,
number = it.number, number = it.number,
start = it.startZoned.toInstant(), start = it.start.toInstant(),
end = it.endZoned.toInstant(), end = it.end.toInstant(),
date = it.date, date = it.date,
subject = it.subject, subject = it.subject,
subjectOld = it.subjectOld, subjectOld = it.subjectOld,
@ -45,8 +45,8 @@ fun List<SdkTimetableAdditional>.mapToEntities(semester: Semester) = map {
diaryId = semester.diaryId, diaryId = semester.diaryId,
subject = it.subject, subject = it.subject,
date = it.date, date = it.date,
start = it.startZoned.toInstant(), start = it.start.toInstant(),
end = it.endZoned.toInstant(), end = it.end.toInstant(),
) )
} }

View File

@ -1,20 +1,25 @@
package io.github.wulkanowy.data.pojos package io.github.wulkanowy.data.pojos
import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.sdk.scrapper.Scrapper import io.github.wulkanowy.sdk.scrapper.Scrapper
data class RegisterUser( data class RegisterUser(
val email: String, val email: String,
val password: String, val password: String?,
val login: String, // may be the same as email val login: String, // may be the same as email
val baseUrl: String, val scrapperBaseUrl: String?,
val loginType: Scrapper.LoginType, val loginType: Scrapper.LoginType?,
val loginMode: Sdk.Mode,
val symbols: List<RegisterSymbol>, val symbols: List<RegisterSymbol>,
) : java.io.Serializable ) : java.io.Serializable
data class RegisterSymbol( data class RegisterSymbol(
val symbol: String, val symbol: String,
val error: Throwable?, val error: Throwable?,
val hebeBaseUrl: String?,
val keyId: String?,
val privatePem: String?,
val userName: String, val userName: String,
val schools: List<RegisterUnit>, val schools: List<RegisterUnit>,
) : java.io.Serializable ) : java.io.Serializable

View File

@ -19,7 +19,6 @@ class AppCreatorRepository @Inject constructor(
) { ) {
@OptIn(ExperimentalSerializationApi::class) @OptIn(ExperimentalSerializationApi::class)
@Suppress("BlockingMethodInNonBlockingContext")
suspend fun getAppCreators() = withContext(dispatchers.io) { suspend fun getAppCreators() = withContext(dispatchers.io) {
val inputStream = context.assets.open("contributors.json").buffered() val inputStream = context.assets.open("contributors.json").buffered()
json.decodeFromStream<List<Contributor>>(inputStream) json.decodeFromStream<List<Contributor>>(inputStream)

View File

@ -59,7 +59,7 @@ class AttendanceRepository @Inject constructor(
} }
sdk.init(student) sdk.init(student)
.switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear)
.getAttendance(start.monday, end.sunday, semester.semesterId) .getAttendance(start.monday, end.sunday)
.mapToEntities(semester, lessons) .mapToEntities(semester, lessons)
}, },
saveFetchResult = { old, new -> saveFetchResult = { old, new ->

View File

@ -52,7 +52,7 @@ class ExamRepository @Inject constructor(
fetch = { fetch = {
sdk.init(student) sdk.init(student)
.switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear)
.getExams(start.startExamsDay, start.endExamsDay, semester.semesterId) .getExams(start.startExamsDay, start.endExamsDay)
.mapToEntities(semester) .mapToEntities(semester)
}, },
saveFetchResult = { old, new -> saveFetchResult = { old, new ->

View File

@ -42,7 +42,7 @@ class NoteRepository @Inject constructor(
fetch = { fetch = {
sdk.init(student) sdk.init(student)
.switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear)
.getNotes(semester.semesterId) .getNotes()
.mapToEntities(semester) .mapToEntities(semester)
}, },
saveFetchResult = { old, new -> saveFetchResult = { old, new ->

View File

@ -2,14 +2,17 @@ package io.github.wulkanowy.data.repositories
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import androidx.annotation.StringRes
import androidx.core.content.edit import androidx.core.content.edit
import com.fredporciuncula.flow.preferences.FlowSharedPreferences import com.fredporciuncula.flow.preferences.FlowSharedPreferences
import com.fredporciuncula.flow.preferences.Preference import com.fredporciuncula.flow.preferences.Preference
import com.fredporciuncula.flow.preferences.Serializer
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.enums.* import io.github.wulkanowy.data.enums.*
import io.github.wulkanowy.ui.modules.dashboard.DashboardItem import io.github.wulkanowy.ui.modules.dashboard.DashboardItem
import io.github.wulkanowy.ui.modules.grade.GradeAverageMode import io.github.wulkanowy.ui.modules.grade.GradeAverageMode
import io.github.wulkanowy.ui.modules.settings.appearance.menuorder.AppMenuItem
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
@ -28,29 +31,35 @@ class PreferencesRepository @Inject constructor(
private val json: Json, private val json: Json,
) { ) {
val startMenuIndex: Int
get() = getString(R.string.pref_key_start_menu, R.string.pref_default_startup).toInt()
val isShowPresent: Boolean val isShowPresent: Boolean
get() = getBoolean( get() = getBoolean(
R.string.pref_key_attendance_present, R.string.pref_key_attendance_present,
R.bool.pref_default_attendance_present R.bool.pref_default_attendance_present
) )
val gradeAverageMode: GradeAverageMode private val gradeAverageModePref: Preference<GradeAverageMode>
get() = GradeAverageMode.getByValue( get() = getObjectFlow(
getString( R.string.pref_key_grade_average_mode,
R.string.pref_key_grade_average_mode, R.string.pref_default_grade_average_mode,
R.string.pref_default_grade_average_mode object : Serializer<GradeAverageMode> {
) override fun serialize(value: GradeAverageMode) = value.value
override fun deserialize(serialized: String) =
GradeAverageMode.getByValue(serialized)
},
) )
val gradeAverageForceCalc: Boolean val gradeAverageModeFlow: Flow<GradeAverageMode>
get() = getBoolean( get() = gradeAverageModePref.asFlow()
R.string.pref_key_grade_average_force_calc,
R.bool.pref_default_grade_average_force_calc private val gradeAverageForceCalcPref: Preference<Boolean>
get() = flowSharedPref.getBoolean(
context.getString(R.string.pref_key_grade_average_force_calc),
context.resources.getBoolean(R.bool.pref_default_grade_average_force_calc)
) )
val gradeAverageForceCalcFlow: Flow<Boolean>
get() = gradeAverageForceCalcPref.asFlow()
val gradeExpandMode: GradeExpandMode val gradeExpandMode: GradeExpandMode
get() = GradeExpandMode.getByValue( get() = GradeExpandMode.getByValue(
getString( getString(
@ -140,12 +149,24 @@ class PreferencesRepository @Inject constructor(
R.string.pref_default_grade_modifier_plus R.string.pref_default_grade_modifier_plus
).toDouble() ).toDouble()
val gradePlusModifierFlow: Flow<Double>
get() = getStringFlow(
R.string.pref_key_grade_modifier_plus,
R.string.pref_default_grade_modifier_plus
).asFlow().map { it.toDouble() }
val gradeMinusModifier: Double val gradeMinusModifier: Double
get() = getString( get() = getString(
R.string.pref_key_grade_modifier_minus, R.string.pref_key_grade_modifier_minus,
R.string.pref_default_grade_modifier_minus R.string.pref_default_grade_modifier_minus
).toDouble() ).toDouble()
val gradeMinusModifierFlow: Flow<Double>
get() = getStringFlow(
R.string.pref_key_grade_modifier_minus,
R.string.pref_default_grade_modifier_minus
).asFlow().map { it.toDouble() }
val fillMessageContent: Boolean val fillMessageContent: Boolean
get() = getBoolean( get() = getBoolean(
R.string.pref_key_fill_message_content, R.string.pref_key_fill_message_content,
@ -180,24 +201,17 @@ class PreferencesRepository @Inject constructor(
R.bool.pref_default_timetable_show_timers R.bool.pref_default_timetable_show_timers
) )
var isHomeworkFullscreen: Boolean
get() = getBoolean(
R.string.pref_key_homework_fullscreen,
R.bool.pref_default_homework_fullscreen
)
set(value) = sharedPref.edit().putBoolean("homework_fullscreen", value).apply()
val showSubjectsWithoutGrades: Boolean val showSubjectsWithoutGrades: Boolean
get() = getBoolean( get() = getBoolean(
R.string.pref_key_subjects_without_grades, R.string.pref_key_subjects_without_grades,
R.bool.pref_default_subjects_without_grades R.bool.pref_default_subjects_without_grades
) )
val isOptionalArithmeticAverage: Boolean val isOptionalArithmeticAverageFlow: Flow<Boolean>
get() = getBoolean( get() = flowSharedPref.getBoolean(
R.string.pref_key_optional_arithmetic_average, context.getString(R.string.pref_key_optional_arithmetic_average),
R.bool.pref_default_optional_arithmetic_average context.resources.getBoolean(R.bool.pref_default_optional_arithmetic_average)
) ).asFlow()
var lasSyncDate: Instant? var lasSyncDate: Instant?
get() = getLong(R.string.pref_key_last_sync_date, R.string.pref_default_last_sync_date) get() = getLong(R.string.pref_key_last_sync_date, R.string.pref_default_last_sync_date)
@ -315,6 +329,20 @@ class PreferencesRepository @Inject constructor(
putBoolean(context.getString(R.string.pref_key_ads_enabled), value) putBoolean(context.getString(R.string.pref_key_ads_enabled), value)
} }
var appMenuItemOrder: List<AppMenuItem>
get() {
val value = sharedPref.getString(PREF_KEY_APP_MENU_ITEM_ORDER, null)
?: return AppMenuItem.defaultAppMenuItemList
return json.decodeFromString(value)
}
set(value) = sharedPref.edit {
putString(
PREF_KEY_APP_MENU_ITEM_ORDER,
json.encodeToString(value)
)
}
var installationId: String var installationId: String
get() = sharedPref.getString(PREF_KEY_INSTALLATION_ID, null).orEmpty() get() = sharedPref.getString(PREF_KEY_INSTALLATION_ID, null).orEmpty()
private set(value) = sharedPref.edit { putString(PREF_KEY_INSTALLATION_ID, value) } private set(value) = sharedPref.edit { putString(PREF_KEY_INSTALLATION_ID, value) }
@ -330,6 +358,21 @@ class PreferencesRepository @Inject constructor(
private fun getLong(id: String, default: Int) = private fun getLong(id: String, default: Int) =
sharedPref.getLong(id, context.resources.getString(default).toLong()) sharedPref.getLong(id, context.resources.getString(default).toLong())
private fun getStringFlow(id: Int, default: Int) =
flowSharedPref.getString(context.getString(id), context.getString(default))
private fun <T : Any> getObjectFlow(
@StringRes id: Int,
@StringRes default: Int,
serializer: Serializer<T>
): Preference<T> = flowSharedPref.getObject(
key = context.getString(id),
serializer = serializer,
defaultValue = serializer.deserialize(
flowSharedPref.getString(context.getString(default)).get()
)
)
private fun getString(id: Int, default: Int) = getString(context.getString(id), default) private fun getString(id: Int, default: Int) = getString(context.getString(id), default)
private fun getString(id: String, default: Int) = private fun getString(id: String, default: Int) =
@ -341,6 +384,7 @@ class PreferencesRepository @Inject constructor(
sharedPref.getBoolean(id, context.resources.getBoolean(default)) sharedPref.getBoolean(id, context.resources.getBoolean(default))
private companion object { private companion object {
private const val PREF_KEY_APP_MENU_ITEM_ORDER = "app_menu_item_order"
private const val PREF_KEY_INSTALLATION_ID = "installation_id" private const val PREF_KEY_INSTALLATION_ID = "installation_id"
private const val PREF_KEY_DASHBOARD_ITEMS_POSITION = "dashboard_items_position" private const val PREF_KEY_DASHBOARD_ITEMS_POSITION = "dashboard_items_position"
private const val PREF_KEY_IN_APP_REVIEW_COUNT = "in_app_review_count" private const val PREF_KEY_IN_APP_REVIEW_COUNT = "in_app_review_count"

View File

@ -40,7 +40,7 @@ class SemesterRepository @Inject constructor(
val isNoSemesters = semesters.isEmpty() val isNoSemesters = semesters.isEmpty()
val isRefreshOnModeChangeRequired = when { val isRefreshOnModeChangeRequired = when {
Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API -> { Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE -> {
semesters.firstOrNull { it.isCurrent }?.let { semesters.firstOrNull { it.isCurrent }?.let {
0 == it.diaryId && 0 == it.kindergartenDiaryId 0 == it.diaryId && 0 == it.kindergartenDiaryId
} == true } == true

View File

@ -10,11 +10,9 @@ import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentNickAndAvatar 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.mapToPojo import io.github.wulkanowy.data.mappers.mapToPojo
import io.github.wulkanowy.data.pojos.RegisterUser 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.DispatchersProvider import io.github.wulkanowy.utils.DispatchersProvider
import io.github.wulkanowy.utils.security.decrypt import io.github.wulkanowy.utils.security.decrypt
import io.github.wulkanowy.utils.security.encrypt import io.github.wulkanowy.utils.security.encrypt
@ -29,37 +27,35 @@ class StudentRepository @Inject constructor(
private val studentDb: StudentDao, private val studentDb: StudentDao,
private val semesterDb: SemesterDao, private val semesterDb: SemesterDao,
private val sdk: Sdk, private val sdk: Sdk,
private val appInfo: AppInfo,
private val appDatabase: AppDatabase private val appDatabase: AppDatabase
) { ) {
suspend fun isStudentSaved() = getSavedStudents(false).isNotEmpty()
suspend fun isCurrentStudentSet() = studentDb.loadCurrent()?.isCurrent ?: false suspend fun isCurrentStudentSet() = studentDb.loadCurrent()?.isCurrent ?: false
suspend fun getStudentsApi( suspend fun getStudentsApi(
pin: String, pin: String,
symbol: String, symbol: String,
token: String token: String
): List<StudentWithSemesters> = ): RegisterUser = sdk
sdk.getStudentsFromMobileApi(token, pin, symbol, "") .getStudentsFromHebe(token, pin, symbol, "")
.mapToEntities(colors = appInfo.defaultColorsForAvatar) .mapToPojo(null)
suspend fun getStudentsScrapper( suspend fun getStudentsScrapper(
email: String, email: String,
password: String, password: String,
scrapperBaseUrl: String, scrapperBaseUrl: String,
symbol: String symbol: String
): List<StudentWithSemesters> = ): RegisterUser = sdk
sdk.getStudentsFromScrapper(email, password, scrapperBaseUrl, symbol) .getUserSubjectsFromScrapper(email, password, scrapperBaseUrl, symbol)
.mapToEntities(password, appInfo.defaultColorsForAvatar) .mapToPojo(password)
suspend fun getUserSubjectsFromScrapper( suspend fun getUserSubjectsFromScrapper(
email: String, email: String,
password: String, password: String,
scrapperBaseUrl: String, scrapperBaseUrl: String,
symbol: String symbol: String
): RegisterUser = sdk.getUserSubjectsFromScrapper(email, password, scrapperBaseUrl, symbol) ): RegisterUser = sdk
.getUserSubjectsFromScrapper(email, password, scrapperBaseUrl, symbol)
.mapToPojo(password) .mapToPojo(password)
suspend fun getStudentsHybrid( suspend fun getStudentsHybrid(
@ -67,15 +63,15 @@ class StudentRepository @Inject constructor(
password: String, password: String,
scrapperBaseUrl: String, scrapperBaseUrl: String,
symbol: String symbol: String
): List<StudentWithSemesters> = ): RegisterUser = sdk
sdk.getStudentsHybrid(email, password, scrapperBaseUrl, "", symbol) .getStudentsHybrid(email, password, scrapperBaseUrl, "", symbol)
.mapToEntities(password, appInfo.defaultColorsForAvatar) .mapToPojo(password)
suspend fun getSavedStudents(decryptPass: Boolean = true) = suspend fun getSavedStudents(decryptPass: Boolean = true) =
studentDb.loadStudentsWithSemesters() studentDb.loadStudentsWithSemesters()
.map { .map {
it.apply { it.apply {
if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API) { if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE) {
student.password = withContext(dispatchers.io) { student.password = withContext(dispatchers.io) {
decrypt(student.password) decrypt(student.password)
} }
@ -85,7 +81,7 @@ class StudentRepository @Inject constructor(
suspend fun getSavedStudentById(id: Long, decryptPass: Boolean = true) = suspend fun getSavedStudentById(id: Long, decryptPass: Boolean = true) =
studentDb.loadStudentWithSemestersById(id)?.apply { studentDb.loadStudentWithSemestersById(id)?.apply {
if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API) { if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE) {
student.password = withContext(dispatchers.io) { student.password = withContext(dispatchers.io) {
decrypt(student.password) decrypt(student.password)
} }
@ -95,7 +91,7 @@ class StudentRepository @Inject constructor(
suspend fun getStudentById(id: Long, decryptPass: Boolean = true): Student { suspend fun getStudentById(id: Long, decryptPass: Boolean = true): Student {
val student = studentDb.loadById(id) ?: throw NoCurrentStudentException() val student = studentDb.loadById(id) ?: throw NoCurrentStudentException()
if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API) { if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE) {
student.password = withContext(dispatchers.io) { student.password = withContext(dispatchers.io) {
decrypt(student.password) decrypt(student.password)
} }
@ -106,7 +102,7 @@ class StudentRepository @Inject constructor(
suspend fun getCurrentStudent(decryptPass: Boolean = true): Student { suspend fun getCurrentStudent(decryptPass: Boolean = true): Student {
val student = studentDb.loadCurrent() ?: throw NoCurrentStudentException() val student = studentDb.loadCurrent() ?: throw NoCurrentStudentException()
if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API) { if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE) {
student.password = withContext(dispatchers.io) { student.password = withContext(dispatchers.io) {
decrypt(student.password) decrypt(student.password)
} }
@ -119,7 +115,7 @@ class StudentRepository @Inject constructor(
val students = studentsWithSemesters.map { it.student } val students = studentsWithSemesters.map { it.student }
.map { .map {
it.apply { it.apply {
if (Sdk.Mode.valueOf(it.loginMode) != Sdk.Mode.API) { if (Sdk.Mode.valueOf(it.loginMode) != Sdk.Mode.HEBE) {
password = withContext(dispatchers.io) { password = withContext(dispatchers.io) {
encrypt(password, context) encrypt(password, context)
} }

View File

@ -40,7 +40,7 @@ class TeacherRepository @Inject constructor(
fetch = { fetch = {
sdk.init(student) sdk.init(student)
.switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear)
.getTeachers(semester.semesterId) .getTeachers()
.mapToEntities(semester) .mapToEntities(semester)
}, },
saveFetchResult = { old, new -> saveFetchResult = { old, new ->

View File

@ -13,6 +13,7 @@ import io.github.wulkanowy.utils.*
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import java.time.Instant
import java.time.LocalDate import java.time.LocalDate
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -65,7 +66,7 @@ class TimetableRepository @Inject constructor(
fetch = { fetch = {
val timetableFull = sdk.init(student) val timetableFull = sdk.init(student)
.switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear)
.getTimetableFull(start.monday, end.sunday) .getTimetable(start.monday, end.sunday)
timetableFull.mapToEntities(semester) timetableFull.mapToEntities(semester)
}, },
@ -164,6 +165,11 @@ class TimetableRepository @Inject constructor(
timetableHeaderDb.insertAll(new uniqueSubtract old) timetableHeaderDb.insertAll(new uniqueSubtract old)
} }
fun getLastRefreshTimestamp(semester: Semester, start: LocalDate, end: LocalDate): Instant {
val refreshKey = getRefreshKey(cacheKey, semester, start, end)
return refreshHelper.getLastRefreshTimestamp(refreshKey)
}
suspend fun saveAdditionalList(additionalList: List<TimetableAdditional>) = suspend fun saveAdditionalList(additionalList: List<TimetableAdditional>) =
timetableAdditionalDb.insertAll(additionalList) timetableAdditionalDb.insertAll(additionalList)

View File

@ -4,18 +4,12 @@ import android.os.Build.VERSION.SDK_INT
import android.os.Build.VERSION_CODES.O import android.os.Build.VERSION_CODES.O
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.lifecycle.asFlow import androidx.lifecycle.asFlow
import androidx.work.*
import androidx.work.BackoffPolicy.EXPONENTIAL import androidx.work.BackoffPolicy.EXPONENTIAL
import androidx.work.Constraints
import androidx.work.Data
import androidx.work.ExistingPeriodicWorkPolicy.KEEP import androidx.work.ExistingPeriodicWorkPolicy.KEEP
import androidx.work.ExistingPeriodicWorkPolicy.REPLACE import androidx.work.ExistingPeriodicWorkPolicy.UPDATE
import androidx.work.ExistingWorkPolicy
import androidx.work.NetworkType.CONNECTED import androidx.work.NetworkType.CONNECTED
import androidx.work.NetworkType.UNMETERED import androidx.work.NetworkType.UNMETERED
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkInfo
import androidx.work.WorkManager
import io.github.wulkanowy.data.db.SharedPrefProvider import io.github.wulkanowy.data.db.SharedPrefProvider
import io.github.wulkanowy.data.db.SharedPrefProvider.Companion.APP_VERSION_CODE_KEY import io.github.wulkanowy.data.db.SharedPrefProvider.Companion.APP_VERSION_CODE_KEY
import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.PreferencesRepository
@ -60,7 +54,7 @@ class SyncManager @Inject constructor(
val serviceInterval = preferencesRepository.servicesInterval val serviceInterval = preferencesRepository.servicesInterval
workManager.enqueueUniquePeriodicWork( workManager.enqueueUniquePeriodicWork(
SyncWorker::class.java.simpleName, if (restart) REPLACE else KEEP, SyncWorker::class.java.simpleName, if (restart) UPDATE else KEEP,
PeriodicWorkRequestBuilder<SyncWorker>(serviceInterval, MINUTES) PeriodicWorkRequestBuilder<SyncWorker>(serviceInterval, MINUTES)
.setInitialDelay(10, MINUTES) .setInitialDelay(10, MINUTES)
.setBackoffCriteria(EXPONENTIAL, 30, MINUTES) .setBackoffCriteria(EXPONENTIAL, 30, MINUTES)

View File

@ -4,7 +4,6 @@ import android.content.Intent
import android.widget.RemoteViewsService import android.widget.RemoteViewsService
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.data.db.SharedPrefProvider import io.github.wulkanowy.data.db.SharedPrefProvider
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.SemesterRepository import io.github.wulkanowy.data.repositories.SemesterRepository
import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.repositories.TimetableRepository import io.github.wulkanowy.data.repositories.TimetableRepository
@ -24,14 +23,13 @@ class TimetableWidgetService : RemoteViewsService() {
@Inject @Inject
lateinit var semesterRepo: SemesterRepository lateinit var semesterRepo: SemesterRepository
@Inject
lateinit var prefRepository: PreferencesRepository
@Inject @Inject
lateinit var sharedPref: SharedPrefProvider lateinit var sharedPref: SharedPrefProvider
override fun onGetViewFactory(intent: Intent?): RemoteViewsFactory { override fun onGetViewFactory(intent: Intent?): RemoteViewsFactory {
Timber.d("TimetableWidgetFactory created") Timber.d("TimetableWidgetFactory created")
return TimetableWidgetFactory(timetableRepo, studentRepo, semesterRepo, prefRepository, sharedPref, applicationContext, intent) return TimetableWidgetFactory(
timetableRepo, studentRepo, semesterRepo, sharedPref, applicationContext, intent
)
} }
} }

View File

@ -4,9 +4,9 @@ import android.app.ActivityManager
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.google.android.material.snackbar.Snackbar.LENGTH_LONG import com.google.android.material.snackbar.Snackbar.LENGTH_LONG
import io.github.wulkanowy.R import io.github.wulkanowy.R
@ -30,6 +30,8 @@ abstract class BaseActivity<T : BasePresenter<out BaseView>, VB : ViewBinding> :
protected var messageContainer: View? = null protected var messageContainer: View? = null
protected var messageAnchor: View? = null
abstract var presenter: T abstract var presenter: T
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -48,6 +50,7 @@ abstract class BaseActivity<T : BasePresenter<out BaseView>, VB : ViewBinding> :
if (messageContainer != null) { if (messageContainer != null) {
Snackbar.make(messageContainer!!, text, LENGTH_LONG) Snackbar.make(messageContainer!!, text, LENGTH_LONG)
.setAction(R.string.all_details) { showErrorDetailsDialog(error) } .setAction(R.string.all_details) { showErrorDetailsDialog(error) }
.apply { messageAnchor?.let { anchorView = it } }
.show() .show()
} else showMessage(text) } else showMessage(text)
} }
@ -57,12 +60,15 @@ abstract class BaseActivity<T : BasePresenter<out BaseView>, VB : ViewBinding> :
} }
override fun showMessage(text: String) { override fun showMessage(text: String) {
if (messageContainer != null) Snackbar.make(messageContainer!!, text, LENGTH_LONG).show() if (messageContainer != null) {
else Toast.makeText(this, text, Toast.LENGTH_LONG).show() Snackbar.make(messageContainer!!, text, LENGTH_LONG)
.apply { messageAnchor?.let { anchorView = it } }
.show()
} else Toast.makeText(this, text, Toast.LENGTH_LONG).show()
} }
override fun showExpiredDialog() { override fun showExpiredDialog() {
AlertDialog.Builder(this) MaterialAlertDialogBuilder(this)
.setTitle(R.string.main_session_expired) .setTitle(R.string.main_session_expired)
.setMessage(R.string.main_session_relogin) .setMessage(R.string.main_session_relogin)
.setPositiveButton(R.string.main_log_in) { _, _ -> presenter.onExpiredLoginSelected() } .setPositiveButton(R.string.main_log_in) { _, _ -> presenter.onExpiredLoginSelected() }
@ -74,6 +80,7 @@ abstract class BaseActivity<T : BasePresenter<out BaseView>, VB : ViewBinding> :
messageContainer?.let { messageContainer?.let {
Snackbar.make(it, R.string.error_password_change_required, LENGTH_LONG) Snackbar.make(it, R.string.error_password_change_required, LENGTH_LONG)
.setAction(R.string.all_change) { openInternetBrowser(redirectUrl) } .setAction(R.string.all_change) { openInternetBrowser(redirectUrl) }
.apply { messageAnchor?.let { anchorView = it } }
.show() .show()
} }
} }

View File

@ -1,8 +1,14 @@
package io.github.wulkanowy.ui.base package io.github.wulkanowy.ui.base
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast import android.widget.Toast
import androidx.annotation.CallSuper
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import com.google.android.material.elevation.SurfaceColors
import io.github.wulkanowy.utils.AnalyticsHelper import io.github.wulkanowy.utils.AnalyticsHelper
import io.github.wulkanowy.utils.lifecycleAwareVariable import io.github.wulkanowy.utils.lifecycleAwareVariable
import javax.inject.Inject import javax.inject.Inject
@ -38,6 +44,19 @@ abstract class BaseDialogFragment<VB : ViewBinding> : DialogFragment(), BaseView
ErrorDialog.newInstance(error).show(childFragmentManager, error.toString()) ErrorDialog.newInstance(error).show(childFragmentManager, error.toString())
} }
@CallSuper
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
view.setBackgroundColor(SurfaceColors.SURFACE_3.getColor(requireContext()))
}
@CallSuper
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
) = binding.root
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
analyticsHelper.setCurrentScreen(requireActivity(), this::class.simpleName) analyticsHelper.setCurrentScreen(requireActivity(), this::class.simpleName)

View File

@ -4,13 +4,13 @@ import android.app.Dialog
import android.content.ClipData import android.content.ClipData
import android.content.ClipboardManager import android.content.ClipboardManager
import android.os.Bundle import android.os.Bundle
import android.view.View
import android.widget.Toast import android.widget.Toast
import android.widget.Toast.LENGTH_LONG import android.widget.Toast.LENGTH_LONG
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.fragment.app.DialogFragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R import io.github.wulkanowy.R
@ -20,7 +20,7 @@ import io.github.wulkanowy.utils.*
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class ErrorDialog : DialogFragment() { class ErrorDialog : BaseDialogFragment<DialogErrorBinding>() {
@Inject @Inject
lateinit var appInfo: AppInfo lateinit var appInfo: AppInfo
@ -28,6 +28,8 @@ class ErrorDialog : DialogFragment() {
@Inject @Inject
lateinit var preferencesRepository: PreferencesRepository lateinit var preferencesRepository: PreferencesRepository
private lateinit var error: Throwable
companion object { companion object {
private const val ARGUMENT_KEY = "error" private const val ARGUMENT_KEY = "error"
@ -36,32 +38,31 @@ class ErrorDialog : DialogFragment() {
} }
} }
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreate(savedInstanceState: Bundle?) {
val error = requireArguments().serializable<Throwable>(ARGUMENT_KEY) super.onCreate(savedInstanceState)
error = requireArguments().serializable(ARGUMENT_KEY)
val binding = DialogErrorBinding.inflate(layoutInflater)
binding.bindErrorDetails(error)
return getAlertDialog(binding, error).apply {
enableReportButtonIfErrorIsReportable(error)
}
} }
private fun getAlertDialog(binding: DialogErrorBinding, error: Throwable): AlertDialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return MaterialAlertDialogBuilder(requireContext()).apply { return MaterialAlertDialogBuilder(requireContext()).apply {
val errorStacktrace = error.stackTraceToString() val errorStacktrace = error.stackTraceToString()
setTitle(R.string.all_details) setTitle(R.string.all_details)
setView(binding.root) setView(DialogErrorBinding.inflate(layoutInflater).apply { binding = this }.root)
setNeutralButton(R.string.about_feedback) { _, _ -> setNeutralButton(R.string.about_feedback) { _, _ ->
openConfirmDialog { openEmailClient(errorStacktrace) } openConfirmDialog { openEmailClient(errorStacktrace) }
} }
setNegativeButton(android.R.string.cancel) { _, _ -> } setNegativeButton(android.R.string.cancel) { _, _ -> }
setPositiveButton(android.R.string.copy) { _, _ -> copyErrorToClipboard(errorStacktrace) } setPositiveButton(android.R.string.copy) { _, _ -> copyErrorToClipboard(errorStacktrace) }
}.create() }.create().apply {
setOnShowListener {
getButton(AlertDialog.BUTTON_NEUTRAL).isEnabled = error.isShouldBeReported()
}
}
} }
private fun DialogErrorBinding.bindErrorDetails(error: Throwable) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
return with(this) { super.onViewCreated(view, savedInstanceState)
with(binding) {
errorDialogHumanizedMessage.text = resources.getErrorString(error) errorDialogHumanizedMessage.text = resources.getErrorString(error)
errorDialogErrorMessage.text = error.localizedMessage errorDialogErrorMessage.text = error.localizedMessage
errorDialogErrorMessage.isGone = error.localizedMessage.isNullOrBlank() errorDialogErrorMessage.isGone = error.localizedMessage.isNullOrBlank()
@ -70,12 +71,6 @@ class ErrorDialog : DialogFragment() {
} }
} }
private fun AlertDialog.enableReportButtonIfErrorIsReportable(error: Throwable) {
setOnShowListener {
getButton(AlertDialog.BUTTON_NEUTRAL).isEnabled = error.isShouldBeReported()
}
}
private fun copyErrorToClipboard(errorStacktrace: String) { private fun copyErrorToClipboard(errorStacktrace: String) {
val clip = ClipData.newPlainText("Error details", errorStacktrace) val clip = ClipData.newPlainText("Error details", errorStacktrace)
requireActivity().getSystemService<ClipboardManager>()?.setPrimaryClip(clip) requireActivity().getSystemService<ClipboardManager>()?.setPrimaryClip(clip)
@ -83,7 +78,7 @@ class ErrorDialog : DialogFragment() {
} }
private fun openConfirmDialog(callback: () -> Unit) { private fun openConfirmDialog(callback: () -> Unit) {
AlertDialog.Builder(requireContext()) MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.dialog_error_check_update) .setTitle(R.string.dialog_error_check_update)
.setMessage(R.string.dialog_error_check_update_message) .setMessage(R.string.dialog_error_check_update_message)
.setNeutralButton(R.string.about_feedback) { _, _ -> callback() } .setNeutralButton(R.string.about_feedback) { _, _ -> callback() }
@ -113,8 +108,4 @@ class ErrorDialog : DialogFragment() {
} }
) )
} }
private fun showMessage(text: String) {
Toast.makeText(requireContext(), text, LENGTH_LONG).show()
}
} }

View File

@ -6,15 +6,14 @@ import android.content.pm.PackageManager.GET_ACTIVITIES
import android.os.Build 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 com.google.android.material.color.DynamicColors
import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_NO
import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_YES
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.enums.AppTheme import io.github.wulkanowy.data.enums.AppTheme
import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.ui.modules.login.LoginActivity import io.github.wulkanowy.ui.modules.login.LoginActivity
import io.github.wulkanowy.ui.modules.luckynumberwidget.LuckyNumberWidgetConfigureActivity
import io.github.wulkanowy.ui.modules.main.MainActivity import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.message.send.SendMessageActivity import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetConfigureActivity
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -28,18 +27,19 @@ class ThemeManager @Inject constructor(private val preferencesRepository: Prefer
when (activity) { when (activity) {
is MainActivity -> activity.setTheme(R.style.WulkanowyTheme_Black) is MainActivity -> activity.setTheme(R.style.WulkanowyTheme_Black)
is LoginActivity -> activity.setTheme(R.style.WulkanowyTheme_Login_Black) is LoginActivity -> activity.setTheme(R.style.WulkanowyTheme_Login_Black)
is SendMessageActivity -> activity.setTheme(R.style.WulkanowyTheme_MessageSend_Black)
} }
} }
} else if (activity is TimetableWidgetConfigureActivity || activity is LuckyNumberWidgetConfigureActivity) {
DynamicColors.applyToActivityIfAvailable(activity)
} }
} }
fun applyDefaultTheme() { fun applyDefaultTheme() {
AppCompatDelegate.setDefaultNightMode( AppCompatDelegate.setDefaultNightMode(
when (preferencesRepository.appTheme) { when (preferencesRepository.appTheme) {
AppTheme.LIGHT -> MODE_NIGHT_NO AppTheme.LIGHT -> AppCompatDelegate.MODE_NIGHT_NO
AppTheme.DARK, AppTheme.BLACK -> MODE_NIGHT_YES AppTheme.DARK, AppTheme.BLACK -> AppCompatDelegate.MODE_NIGHT_YES
AppTheme.SYSTEM -> MODE_NIGHT_FOLLOW_SYSTEM AppTheme.SYSTEM -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
} }
) )
} }
@ -52,7 +52,6 @@ class ThemeManager @Inject constructor(private val preferencesRepository: Prefer
.let { .let {
it == R.style.WulkanowyTheme_Black || it == R.style.WulkanowyTheme_NoActionBar it == R.style.WulkanowyTheme_Black || it == R.style.WulkanowyTheme_NoActionBar
|| 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
} }
@Suppress("DEPRECATION") @Suppress("DEPRECATION")

View File

@ -9,11 +9,14 @@ import io.github.wulkanowy.ui.modules.exam.ExamFragment
import io.github.wulkanowy.ui.modules.grade.GradeFragment import io.github.wulkanowy.ui.modules.grade.GradeFragment
import io.github.wulkanowy.ui.modules.homework.HomeworkFragment import io.github.wulkanowy.ui.modules.homework.HomeworkFragment
import io.github.wulkanowy.ui.modules.luckynumber.LuckyNumberFragment import io.github.wulkanowy.ui.modules.luckynumber.LuckyNumberFragment
import io.github.wulkanowy.ui.modules.luckynumber.history.LuckyNumberHistoryFragment
import io.github.wulkanowy.ui.modules.message.MessageFragment import io.github.wulkanowy.ui.modules.message.MessageFragment
import io.github.wulkanowy.ui.modules.mobiledevice.MobileDeviceFragment
import io.github.wulkanowy.ui.modules.more.MoreFragment import io.github.wulkanowy.ui.modules.more.MoreFragment
import io.github.wulkanowy.ui.modules.note.NoteFragment import io.github.wulkanowy.ui.modules.note.NoteFragment
import io.github.wulkanowy.ui.modules.schoolandteachers.school.SchoolFragment import io.github.wulkanowy.ui.modules.schoolandteachers.SchoolAndTeachersFragment
import io.github.wulkanowy.ui.modules.schoolannouncement.SchoolAnnouncementFragment import io.github.wulkanowy.ui.modules.schoolannouncement.SchoolAnnouncementFragment
import io.github.wulkanowy.ui.modules.settings.SettingsFragment
import io.github.wulkanowy.ui.modules.timetable.TimetableFragment import io.github.wulkanowy.ui.modules.timetable.TimetableFragment
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import java.time.LocalDate import java.time.LocalDate
@ -39,10 +42,13 @@ sealed class Destination {
NOTE(Note), NOTE(Note),
CONFERENCE(Conference), CONFERENCE(Conference),
SCHOOL_ANNOUNCEMENT(SchoolAnnouncement), SCHOOL_ANNOUNCEMENT(SchoolAnnouncement),
SCHOOL(School), SCHOOL_AND_TEACHERS(SchoolAndTeachers),
LUCKY_NUMBER(More), LUCKY_NUMBER(LuckyNumber),
LUCKY_NUMBER_HISTORY(LuckyNumberHistory),
MORE(More), MORE(More),
MESSAGE(Message); MESSAGE(Message),
MOBILE_DEVICE(MobileDevice),
SETTINGS(Settings);
} }
@Serializable @Serializable
@ -103,9 +109,9 @@ sealed class Destination {
} }
@Serializable @Serializable
object School : Destination() { object SchoolAndTeachers : Destination() {
override val destinationType get() = Type.SCHOOL override val destinationType get() = Type.SCHOOL_AND_TEACHERS
override val destinationFragment get() = SchoolFragment.newInstance() override val destinationFragment get() = SchoolAndTeachersFragment.newInstance()
} }
@Serializable @Serializable
@ -114,6 +120,12 @@ sealed class Destination {
override val destinationFragment get() = LuckyNumberFragment.newInstance() override val destinationFragment get() = LuckyNumberFragment.newInstance()
} }
@Serializable
object LuckyNumberHistory : Destination() {
override val destinationType get() = Type.LUCKY_NUMBER_HISTORY
override val destinationFragment get() = LuckyNumberHistoryFragment.newInstance()
}
@Serializable @Serializable
object More : Destination() { object More : Destination() {
override val destinationType get() = Type.MORE override val destinationType get() = Type.MORE
@ -125,4 +137,16 @@ sealed class Destination {
override val destinationType get() = Type.MESSAGE override val destinationType get() = Type.MESSAGE
override val destinationFragment get() = MessageFragment.newInstance() override val destinationFragment get() = MessageFragment.newInstance()
} }
@Serializable
object MobileDevice : Destination() {
override val destinationType get() = Type.MOBILE_DEVICE
override val destinationFragment get() = MobileDeviceFragment.newInstance()
}
@Serializable
object Settings : Destination() {
override val destinationType get() = Type.SETTINGS
override val destinationFragment get() = SettingsFragment.newInstance()
}
} }

View File

@ -9,6 +9,7 @@ import androidx.appcompat.app.AlertDialog
import androidx.core.os.bundleOf 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 com.google.android.material.dialog.MaterialAlertDialogBuilder
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.Student import io.github.wulkanowy.data.db.entities.Student
@ -114,7 +115,7 @@ class AccountDetailsFragment :
override fun showLogoutConfirmDialog() { override fun showLogoutConfirmDialog() {
context?.let { context?.let {
AlertDialog.Builder(it) MaterialAlertDialogBuilder(it)
.setTitle(R.string.account_logout_student) .setTitle(R.string.account_logout_student)
.setMessage(R.string.account_confirm) .setMessage(R.string.account_confirm)
.setPositiveButton(R.string.account_logout) { _, _ -> presenter.onLogoutConfirm() } .setPositiveButton(R.string.account_logout) { _, _ -> presenter.onLogoutConfirm() }

View File

@ -1,11 +1,11 @@
package io.github.wulkanowy.ui.modules.account.accountedit package io.github.wulkanowy.ui.modules.account.accountedit
import android.app.Dialog
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
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
@ -31,16 +31,12 @@ class AccountEditDialog : BaseDialogFragment<DialogAccountEditBinding>(), Accoun
} }
} }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NO_TITLE, 0)
}
override fun onCreateView( override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
inflater: LayoutInflater, return MaterialAlertDialogBuilder(requireContext(), theme)
container: ViewGroup?, .setView(DialogAccountEditBinding.inflate(layoutInflater).apply { binding = this }.root)
savedInstanceState: Bundle? .create()
): View = DialogAccountEditBinding.inflate(inflater).apply { binding = this }.root }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)

View File

@ -1,11 +1,11 @@
package io.github.wulkanowy.ui.modules.account.accountquick package io.github.wulkanowy.ui.modules.account.accountquick
import android.app.Dialog
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
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
import io.github.wulkanowy.databinding.DialogAccountQuickBinding import io.github.wulkanowy.databinding.DialogAccountQuickBinding
@ -36,19 +36,17 @@ class AccountQuickDialog : BaseDialogFragment<DialogAccountQuickBinding>(), Acco
} }
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
super.onCreate(savedInstanceState) return MaterialAlertDialogBuilder(requireContext(), theme)
setStyle(STYLE_NO_TITLE, 0) .setView(
DialogAccountQuickBinding.inflate(layoutInflater)
.apply { binding = this }.root
)
.create()
} }
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
) = DialogAccountQuickBinding.inflate(inflater).apply { binding = this }.root
@Suppress("UNCHECKED_CAST")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val studentsWithSemesters = requireArguments() val studentsWithSemesters = requireArguments()
.serializable<Array<StudentWithSemesters>>(STUDENTS_ARGUMENT_KEY).toList() .serializable<Array<StudentWithSemesters>>(STUDENTS_ARGUMENT_KEY).toList()

View File

@ -1,21 +1,20 @@
package io.github.wulkanowy.ui.modules.attendance package io.github.wulkanowy.ui.modules.attendance
import android.app.Dialog
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.fragment.app.DialogFragment import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
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.ui.base.BaseDialogFragment
import io.github.wulkanowy.utils.descriptionRes import io.github.wulkanowy.utils.descriptionRes
import io.github.wulkanowy.utils.lifecycleAwareVariable
import io.github.wulkanowy.utils.serializable import io.github.wulkanowy.utils.serializable
import io.github.wulkanowy.utils.toFormattedString import io.github.wulkanowy.utils.toFormattedString
class AttendanceDialog : DialogFragment() { @AndroidEntryPoint
class AttendanceDialog : BaseDialogFragment<DialogAttendanceBinding>() {
private var binding: DialogAttendanceBinding by lifecycleAwareVariable()
private lateinit var attendance: Attendance private lateinit var attendance: Attendance
@ -30,15 +29,14 @@ class AttendanceDialog : DialogFragment() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setStyle(STYLE_NO_TITLE, 0)
attendance = requireArguments().serializable(ARGUMENT_KEY) attendance = requireArguments().serializable(ARGUMENT_KEY)
} }
override fun onCreateView( override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
inflater: LayoutInflater, return MaterialAlertDialogBuilder(requireContext(), theme)
container: ViewGroup?, .setView(DialogAttendanceBinding.inflate(layoutInflater).apply { binding = this }.root)
savedInstanceState: Bundle? .create()
) = DialogAttendanceBinding.inflate(inflater).apply { binding = this }.root }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)

View File

@ -4,10 +4,10 @@ import android.content.DialogInterface.BUTTON_POSITIVE
import android.os.Bundle import android.os.Bundle
import android.view.* import android.view.*
import android.view.View.* import android.view.View.*
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
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.Attendance import io.github.wulkanowy.data.db.entities.Attendance
@ -124,7 +124,7 @@ class AttendanceFragment : BaseFragment<FragmentAttendanceBinding>(R.layout.frag
attendanceExcuseButton.setOnClickListener { presenter.onExcuseButtonClick() } attendanceExcuseButton.setOnClickListener { presenter.onExcuseButtonClick() }
attendanceNavContainer.elevation = requireContext().dpToPx(8f) attendanceNavContainer.elevation = requireContext().dpToPx(3f)
} }
} }
@ -228,7 +228,7 @@ class AttendanceFragment : BaseFragment<FragmentAttendanceBinding>(R.layout.frag
override fun showExcuseDialog() { override fun showExcuseDialog() {
val dialogBinding = DialogExcuseBinding.inflate(LayoutInflater.from(context)) val dialogBinding = DialogExcuseBinding.inflate(LayoutInflater.from(context))
AlertDialog.Builder(requireContext()) MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.attendance_excuse_title) .setTitle(R.string.attendance_excuse_title)
.setView(dialogBinding.root) .setView(dialogBinding.root)
.setNegativeButton(android.R.string.cancel) { _, _ -> } .setNegativeButton(android.R.string.cancel) { _, _ -> }

View File

@ -1,21 +1,20 @@
package io.github.wulkanowy.ui.modules.conference package io.github.wulkanowy.ui.modules.conference
import android.app.Dialog
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.DialogFragment import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
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.ui.base.BaseDialogFragment
import io.github.wulkanowy.utils.serializable import io.github.wulkanowy.utils.serializable
import io.github.wulkanowy.utils.toFormattedString import io.github.wulkanowy.utils.toFormattedString
class ConferenceDialog : DialogFragment() { @AndroidEntryPoint
class ConferenceDialog : BaseDialogFragment<DialogConferenceBinding>() {
private var binding: DialogConferenceBinding by lifecycleAwareVariable()
private lateinit var conference: Conference private lateinit var conference: Conference
@ -30,15 +29,14 @@ class ConferenceDialog : DialogFragment() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setStyle(STYLE_NO_TITLE, 0)
conference = requireArguments().serializable(ARGUMENT_KEY) conference = requireArguments().serializable(ARGUMENT_KEY)
} }
override fun onCreateView( override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
inflater: LayoutInflater, return MaterialAlertDialogBuilder(requireContext(), theme)
container: ViewGroup?, .setView(DialogConferenceBinding.inflate(layoutInflater).apply { binding = this }.root)
savedInstanceState: Bundle? .create()
) = DialogConferenceBinding.inflate(inflater).also { binding = it }.root }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)

View File

@ -16,7 +16,7 @@ import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class ConferenceFragment : BaseFragment<FragmentConferenceBinding>(R.layout.fragment_conference), class ConferenceFragment : BaseFragment<FragmentConferenceBinding>(R.layout.fragment_conference),
ConferenceView, MainView.TitledView { ConferenceView, MainView.TitledView, MainView.MainChildView {
@Inject @Inject
lateinit var presenter: ConferencePresenter lateinit var presenter: ConferencePresenter
@ -109,6 +109,14 @@ class ConferenceFragment : BaseFragment<FragmentConferenceBinding>(R.layout.frag
(activity as? MainActivity)?.showDialogFragment(ConferenceDialog.newInstance(conference)) (activity as? MainActivity)?.showDialogFragment(ConferenceDialog.newInstance(conference))
} }
override fun onFragmentReselected() {
if (::presenter.isInitialized) presenter.onFragmentReselected()
}
override fun resetView() {
binding.conferenceRecycler.smoothScrollToPosition(0)
}
override fun onDestroyView() { override fun onDestroyView() {
presenter.onDetachView() presenter.onDetachView()
super.onDestroyView() super.onDestroyView()

View File

@ -96,4 +96,11 @@ class ConferencePresenter @Inject constructor(
.onResourceError(errorHandler::dispatch) .onResourceError(errorHandler::dispatch)
.launch() .launch()
} }
fun onFragmentReselected() {
Timber.i("Conference is reselected")
if (view?.isViewEmpty == false) {
view?.resetView()
}
}
} }

View File

@ -28,4 +28,6 @@ interface ConferenceView : BaseView {
fun showContent(show: Boolean) fun showContent(show: Boolean)
fun openConferenceDialog(conference: Conference) fun openConferenceDialog(conference: Conference)
fun resetView()
} }

View File

@ -11,6 +11,7 @@ import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.databinding.FragmentDashboardBinding import io.github.wulkanowy.databinding.FragmentDashboardBinding
@ -148,7 +149,7 @@ class DashboardFragment : BaseFragment<FragmentDashboardBinding>(R.layout.fragme
val values = requireContext().resources.getStringArray(R.array.dashboard_tile_values) val values = requireContext().resources.getStringArray(R.array.dashboard_tile_values)
val selectedItemsState = values.map { value -> selectedItems.any { it.name == value } } val selectedItemsState = values.map { value -> selectedItems.any { it.name == value } }
AlertDialog.Builder(requireContext()) MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.pref_dashboard_appearance_tiles_title) .setTitle(R.string.pref_dashboard_appearance_tiles_title)
.setMultiChoiceItems(entries, selectedItemsState.toBooleanArray()) { _, _, _ -> } .setMultiChoiceItems(entries, selectedItemsState.toBooleanArray()) { _, _, _ -> }
.setPositiveButton(android.R.string.ok) { dialog, _ -> .setPositiveButton(android.R.string.ok) { dialog, _ ->

View File

@ -606,7 +606,7 @@ class DashboardPresenter @Inject constructor(
} }
is Resource.Error -> { is Resource.Error -> {
Timber.i("Loading dashboard admin message result: An exception occurred") Timber.i("Loading dashboard admin message result: An exception occurred")
errorHandler.dispatch(it.error) Timber.e(it.error)
updateData( updateData(
dashboardItem = DashboardItem.AdminMessages( dashboardItem = DashboardItem.AdminMessages(
adminMessage = null, adminMessage = null,
@ -748,7 +748,7 @@ class DashboardPresenter @Inject constructor(
itemsLoadedList.find { it.type == DashboardItem.Type.ACCOUNT }?.error != null itemsLoadedList.find { it.type == DashboardItem.Type.ACCOUNT }?.error != null
val isGeneralError = val isGeneralError =
filteredItems.none { it.error == null } && filteredItems.isNotEmpty() || isAccountItemError filteredItems.none { it.error == null } && filteredItems.isNotEmpty() || isAccountItemError
val firstError = itemsLoadedList.mapNotNull { it.error }.firstOrNull() val firstError = itemsLoadedList.firstNotNullOfOrNull { it.error }
val filteredOriginalLoadedList = val filteredOriginalLoadedList =
dashboardItemLoadedList.filterNot { it.type == DashboardItem.Type.ACCOUNT } dashboardItemLoadedList.filterNot { it.type == DashboardItem.Type.ACCOUNT }

View File

@ -1,5 +1,6 @@
package io.github.wulkanowy.ui.modules.dashboard.adapters package io.github.wulkanowy.ui.modules.dashboard.adapters
import android.content.res.ColorStateList
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@ -8,6 +9,7 @@ import io.github.wulkanowy.data.enums.GradeColorTheme
import io.github.wulkanowy.databinding.SubitemDashboardGradesBinding import io.github.wulkanowy.databinding.SubitemDashboardGradesBinding
import io.github.wulkanowy.databinding.SubitemDashboardSmallGradeBinding import io.github.wulkanowy.databinding.SubitemDashboardSmallGradeBinding
import io.github.wulkanowy.utils.getBackgroundColor import io.github.wulkanowy.utils.getBackgroundColor
import io.github.wulkanowy.utils.getCompatColor
class DashboardGradesAdapter : RecyclerView.Adapter<DashboardGradesAdapter.ViewHolder>() { class DashboardGradesAdapter : RecyclerView.Adapter<DashboardGradesAdapter.ViewHolder>() {
@ -37,7 +39,9 @@ class DashboardGradesAdapter : RecyclerView.Adapter<DashboardGradesAdapter.ViewH
with(subitemBinding.dashboardSmallGradeSubitemValue) { with(subitemBinding.dashboardSmallGradeSubitemValue) {
text = it.entry text = it.entry
setBackgroundResource(it.getBackgroundColor(gradeColorTheme)) backgroundTintList = ColorStateList.valueOf(
context.getCompatColor(it.getBackgroundColor(gradeColorTheme))
)
} }
dashboardGradesSubitemGradeContainer.addView(subitemBinding.root) dashboardGradesSubitemGradeContainer.addView(subitemBinding.root)

View File

@ -1,23 +1,22 @@
package io.github.wulkanowy.ui.modules.exam package io.github.wulkanowy.ui.modules.exam
import android.app.Dialog
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.fragment.app.DialogFragment import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
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.ui.base.BaseDialogFragment
import io.github.wulkanowy.utils.openCalendarEventAdd import io.github.wulkanowy.utils.openCalendarEventAdd
import io.github.wulkanowy.utils.serializable 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
class ExamDialog : DialogFragment() { @AndroidEntryPoint
class ExamDialog : BaseDialogFragment<DialogExamBinding>() {
private var binding: DialogExamBinding by lifecycleAwareVariable()
private lateinit var exam: Exam private lateinit var exam: Exam
@ -32,15 +31,14 @@ class ExamDialog : DialogFragment() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setStyle(STYLE_NO_TITLE, 0)
exam = requireArguments().serializable(ARGUMENT_KEY) exam = requireArguments().serializable(ARGUMENT_KEY)
} }
override fun onCreateView( override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
inflater: LayoutInflater, return MaterialAlertDialogBuilder(requireContext(), theme)
container: ViewGroup?, .setView(DialogExamBinding.inflate(layoutInflater).apply { binding = this }.root)
savedInstanceState: Bundle? .create()
) = DialogExamBinding.inflate(inflater).apply { binding = this }.root }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)

View File

@ -2,9 +2,7 @@ package io.github.wulkanowy.ui.modules.exam
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.*
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
@ -20,7 +18,7 @@ import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class ExamFragment : BaseFragment<FragmentExamBinding>(R.layout.fragment_exam), ExamView, class ExamFragment : BaseFragment<FragmentExamBinding>(R.layout.fragment_exam), ExamView,
MainView.TitledView { MainView.TitledView, MainView.MainChildView {
@Inject @Inject
lateinit var presenter: ExamPresenter lateinit var presenter: ExamPresenter
@ -64,7 +62,7 @@ class ExamFragment : BaseFragment<FragmentExamBinding>(R.layout.fragment_exam),
examPreviousButton.setOnClickListener { presenter.onPreviousWeek() } examPreviousButton.setOnClickListener { presenter.onPreviousWeek() }
examNextButton.setOnClickListener { presenter.onNextWeek() } examNextButton.setOnClickListener { presenter.onNextWeek() }
examNavContainer.elevation = requireContext().dpToPx(8f) examNavContainer.elevation = requireContext().dpToPx(3f)
} }
} }
@ -126,6 +124,14 @@ class ExamFragment : BaseFragment<FragmentExamBinding>(R.layout.fragment_exam),
(activity as? MainActivity)?.showDialogFragment(ExamDialog.newInstance(exam)) (activity as? MainActivity)?.showDialogFragment(ExamDialog.newInstance(exam))
} }
override fun onFragmentReselected() {
if (::presenter.isInitialized) presenter.onViewReselected()
}
override fun resetView() {
binding.examRecycler.smoothScrollToPosition(0)
}
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
outState.putLong(SAVED_DATE_KEY, presenter.currentDate.toEpochDay()) outState.putLong(SAVED_DATE_KEY, presenter.currentDate.toEpochDay())

View File

@ -175,4 +175,17 @@ class ExamPresenter @Inject constructor(
) )
} }
} }
fun onViewReselected() {
Timber.i("Exam view is reselected")
baseDate = now().nextOrSameSchoolDay
if (currentDate != baseDate) {
reloadView(baseDate)
loadData()
} else if (view?.isViewEmpty == false) {
view?.resetView()
}
}
} }

View File

@ -34,4 +34,6 @@ interface ExamView : BaseView {
fun showPreButton(show: Boolean) fun showPreButton(show: Boolean)
fun showExamDialog(exam: Exam) fun showExamDialog(exam: Exam)
fun resetView()
} }

View File

@ -12,70 +12,92 @@ import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.ui.modules.grade.GradeAverageMode.* import io.github.wulkanowy.ui.modules.grade.GradeAverageMode.*
import io.github.wulkanowy.utils.calcAverage import io.github.wulkanowy.utils.calcAverage
import io.github.wulkanowy.utils.changeModifier import io.github.wulkanowy.utils.changeModifier
import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import javax.inject.Inject import javax.inject.Inject
@OptIn(FlowPreview::class) @OptIn(ExperimentalCoroutinesApi::class)
class GradeAverageProvider @Inject constructor( class GradeAverageProvider @Inject constructor(
private val semesterRepository: SemesterRepository, private val semesterRepository: SemesterRepository,
private val gradeRepository: GradeRepository, private val gradeRepository: GradeRepository,
private val preferencesRepository: PreferencesRepository private val preferencesRepository: PreferencesRepository
) { ) {
private val plusModifier get() = preferencesRepository.gradePlusModifier private data class AverageCalcParams(
val gradeAverageMode: GradeAverageMode,
val forceAverageCalc: Boolean,
val isOptionalArithmeticAverage: Boolean,
val plusModifier: Double,
val minusModifier: Double,
)
private val minusModifier get() = preferencesRepository.gradeMinusModifier fun getGradesDetailsWithAverage(
student: Student,
private val isOptionalArithmeticAverage get() = preferencesRepository.isOptionalArithmeticAverage semesterId: Int,
forceRefresh: Boolean
fun getGradesDetailsWithAverage(student: Student, semesterId: Int, forceRefresh: Boolean) = ): Flow<Resource<List<GradeSubject>>> = combine(
flow = preferencesRepository.gradeAverageModeFlow,
flow2 = preferencesRepository.gradeAverageForceCalcFlow,
flow3 = preferencesRepository.isOptionalArithmeticAverageFlow,
flow4 = preferencesRepository.gradePlusModifierFlow,
flow5 = preferencesRepository.gradeMinusModifierFlow,
) { gradeAverageMode, forceAverageCalc, isOptionalArithmeticAverage, plusModifier, minusModifier ->
AverageCalcParams(
gradeAverageMode = gradeAverageMode,
forceAverageCalc = forceAverageCalc,
isOptionalArithmeticAverage = isOptionalArithmeticAverage,
plusModifier = plusModifier,
minusModifier = minusModifier,
)
}.flatMapLatest { params ->
flatResourceFlow { flatResourceFlow {
val semesters = semesterRepository.getSemesters(student) val semesters = semesterRepository.getSemesters(student)
when (params.gradeAverageMode) {
when (preferencesRepository.gradeAverageMode) {
ONE_SEMESTER -> getGradeSubjects( ONE_SEMESTER -> getGradeSubjects(
student = student, student = student,
semester = semesters.single { it.semesterId == semesterId }, semester = semesters.single { it.semesterId == semesterId },
forceRefresh = forceRefresh forceRefresh = forceRefresh,
params = params,
) )
BOTH_SEMESTERS -> calculateCombinedAverage( BOTH_SEMESTERS -> calculateCombinedAverage(
student = student, student = student,
semesters = semesters, semesters = semesters,
semesterId = semesterId, semesterId = semesterId,
forceRefresh = forceRefresh, forceRefresh = forceRefresh,
averageMode = BOTH_SEMESTERS config = params,
) )
ALL_YEAR -> calculateCombinedAverage( ALL_YEAR -> calculateCombinedAverage(
student = student, student = student,
semesters = semesters, semesters = semesters,
semesterId = semesterId, semesterId = semesterId,
forceRefresh = forceRefresh, forceRefresh = forceRefresh,
averageMode = ALL_YEAR config = params,
) )
} }
}.distinctUntilChanged() }
}.distinctUntilChanged()
private fun calculateCombinedAverage( private fun calculateCombinedAverage(
student: Student, student: Student,
semesters: List<Semester>, semesters: List<Semester>,
semesterId: Int, semesterId: Int,
forceRefresh: Boolean, forceRefresh: Boolean,
averageMode: GradeAverageMode config: AverageCalcParams,
): Flow<Resource<List<GradeSubject>>> { ): Flow<Resource<List<GradeSubject>>> {
val isGradeAverageForceCalc = preferencesRepository.gradeAverageForceCalc
val selectedSemester = semesters.single { it.semesterId == semesterId } val selectedSemester = semesters.single { it.semesterId == semesterId }
val firstSemester = val firstSemester =
semesters.single { it.diaryId == selectedSemester.diaryId && it.semesterName == 1 } semesters.single { it.diaryId == selectedSemester.diaryId && it.semesterName == 1 }
val selectedSemesterGradeSubjects = val selectedSemesterGradeSubjects =
getGradeSubjects(student, selectedSemester, forceRefresh) getGradeSubjects(student, selectedSemester, forceRefresh, config)
if (selectedSemester == firstSemester) return selectedSemesterGradeSubjects if (selectedSemester == firstSemester) return selectedSemesterGradeSubjects
val firstSemesterGradeSubjects = getGradeSubjects(student, firstSemester, forceRefresh) val firstSemesterGradeSubjects =
getGradeSubjects(student, firstSemester, forceRefresh, config)
return selectedSemesterGradeSubjects.combine(firstSemesterGradeSubjects) { secondSemesterGradeSubject, firstSemesterGradeSubject -> return selectedSemesterGradeSubjects.combine(firstSemesterGradeSubjects) { secondSemesterGradeSubject, firstSemesterGradeSubject ->
if (firstSemesterGradeSubject.errorOrNull != null) { if (firstSemesterGradeSubject.errorOrNull != null) {
@ -91,21 +113,21 @@ class GradeAverageProvider @Inject constructor(
val firstSemesterSubject = firstSemesterGradeSubject.dataOrNull.orEmpty() val firstSemesterSubject = firstSemesterGradeSubject.dataOrNull.orEmpty()
.singleOrNull { it.subject == secondSemesterSubject.subject } .singleOrNull { it.subject == secondSemesterSubject.subject }
val updatedAverage = if (averageMode == ALL_YEAR) { val updatedAverage = if (config.gradeAverageMode == ALL_YEAR) {
calculateAllYearAverage( calculateAllYearAverage(
student = student, student = student,
isAnyVulcanAverage = isAnyVulcanAverageInFirstSemester || isAnyVulcanAverageInSecondSemester, isAnyVulcanAverage = isAnyVulcanAverageInFirstSemester || isAnyVulcanAverageInSecondSemester,
isGradeAverageForceCalc = isGradeAverageForceCalc,
secondSemesterSubject = secondSemesterSubject, secondSemesterSubject = secondSemesterSubject,
firstSemesterSubject = firstSemesterSubject firstSemesterSubject = firstSemesterSubject,
config = config,
) )
} else { } else {
calculateBothSemestersAverage( calculateBothSemestersAverage(
student = student, student = student,
isAnyVulcanAverage = isAnyVulcanAverageInFirstSemester || isAnyVulcanAverageInSecondSemester, isAnyVulcanAverage = isAnyVulcanAverageInFirstSemester || isAnyVulcanAverageInSecondSemester,
isGradeAverageForceCalc = isGradeAverageForceCalc,
secondSemesterSubject = secondSemesterSubject, secondSemesterSubject = secondSemesterSubject,
firstSemesterSubject = firstSemesterSubject firstSemesterSubject = firstSemesterSubject,
config = config
) )
} }
secondSemesterSubject.copy(average = updatedAverage) secondSemesterSubject.copy(average = updatedAverage)
@ -117,17 +139,17 @@ class GradeAverageProvider @Inject constructor(
private fun calculateAllYearAverage( private fun calculateAllYearAverage(
student: Student, student: Student,
isAnyVulcanAverage: Boolean, isAnyVulcanAverage: Boolean,
isGradeAverageForceCalc: Boolean,
secondSemesterSubject: GradeSubject, secondSemesterSubject: GradeSubject,
firstSemesterSubject: GradeSubject? firstSemesterSubject: GradeSubject?,
) = if (!isAnyVulcanAverage || isGradeAverageForceCalc) { config: AverageCalcParams,
val updatedSecondSemesterGrades = ) = if (!isAnyVulcanAverage || config.forceAverageCalc) {
secondSemesterSubject.grades.updateModifiers(student) val updatedSecondSemesterGrades = secondSemesterSubject.grades
val updatedFirstSemesterGrades = .updateModifiers(student, config)
firstSemesterSubject?.grades?.updateModifiers(student).orEmpty() val updatedFirstSemesterGrades = firstSemesterSubject?.grades
?.updateModifiers(student, config).orEmpty()
(updatedSecondSemesterGrades + updatedFirstSemesterGrades).calcAverage( (updatedSecondSemesterGrades + updatedFirstSemesterGrades).calcAverage(
isOptionalArithmeticAverage config.isOptionalArithmeticAverage
) )
} else { } else {
secondSemesterSubject.average secondSemesterSubject.average
@ -136,32 +158,35 @@ class GradeAverageProvider @Inject constructor(
private fun calculateBothSemestersAverage( private fun calculateBothSemestersAverage(
student: Student, student: Student,
isAnyVulcanAverage: Boolean, isAnyVulcanAverage: Boolean,
isGradeAverageForceCalc: Boolean,
secondSemesterSubject: GradeSubject, secondSemesterSubject: GradeSubject,
firstSemesterSubject: GradeSubject? firstSemesterSubject: GradeSubject?,
): Double = if (!isAnyVulcanAverage || isGradeAverageForceCalc) { config: AverageCalcParams,
val divider = if (secondSemesterSubject.grades.any { it.weightValue > .0 }) 2 else 1 ): Double {
return if (!isAnyVulcanAverage || config.forceAverageCalc) {
val divider = if (secondSemesterSubject.grades.any { it.weightValue > .0 }) 2 else 1
val secondSemesterAverage = secondSemesterSubject.grades
.updateModifiers(student, config)
.calcAverage(config.isOptionalArithmeticAverage)
val firstSemesterAverage = firstSemesterSubject?.grades
?.updateModifiers(student, config)
?.calcAverage(config.isOptionalArithmeticAverage) ?: secondSemesterAverage
val secondSemesterAverage = secondSemesterSubject.grades.updateModifiers(student) (secondSemesterAverage + firstSemesterAverage) / divider
.calcAverage(isOptionalArithmeticAverage) } else {
val firstSemesterAverage = firstSemesterSubject?.grades?.updateModifiers(student) val divider = if (secondSemesterSubject.average > 0) 2 else 1
?.calcAverage(isOptionalArithmeticAverage) ?: secondSemesterAverage
(secondSemesterAverage + firstSemesterAverage) / divider secondSemesterSubject.average.plus(
} else { (firstSemesterSubject?.average ?: secondSemesterSubject.average)
val divider = if (secondSemesterSubject.average > 0) 2 else 1 ) / divider
}
(secondSemesterSubject.average + (firstSemesterSubject?.average
?: secondSemesterSubject.average)) / divider
} }
private fun getGradeSubjects( private fun getGradeSubjects(
student: Student, student: Student,
semester: Semester, semester: Semester,
forceRefresh: Boolean forceRefresh: Boolean,
params: AverageCalcParams,
): Flow<Resource<List<GradeSubject>>> { ): Flow<Resource<List<GradeSubject>>> {
val isGradeAverageForceCalc = preferencesRepository.gradeAverageForceCalc
return gradeRepository.getGrades(student, semester, forceRefresh = forceRefresh) return gradeRepository.getGrades(student, semester, forceRefresh = forceRefresh)
.mapResourceData { res -> .mapResourceData { res ->
val (details, summaries) = res val (details, summaries) = res
@ -172,13 +197,15 @@ class GradeAverageProvider @Inject constructor(
student = student, student = student,
semester = semester, semester = semester,
grades = allGrades.toList(), grades = allGrades.toList(),
calcAverage = isAnyAverage calcAverage = isAnyAverage,
params = params,
).map { summary -> ).map { summary ->
val grades = allGrades[summary.subject].orEmpty() val grades = allGrades[summary.subject].orEmpty()
GradeSubject( GradeSubject(
subject = summary.subject, subject = summary.subject,
average = if (!isAnyAverage || isGradeAverageForceCalc) { average = if (!isAnyAverage || params.forceAverageCalc) {
grades.updateModifiers(student).calcAverage(isOptionalArithmeticAverage) grades.updateModifiers(student, params)
.calcAverage(params.isOptionalArithmeticAverage)
} else summary.average, } else summary.average,
points = summary.pointsSum, points = summary.pointsSum,
summary = summary, summary = summary,
@ -195,7 +222,8 @@ class GradeAverageProvider @Inject constructor(
student: Student, student: Student,
semester: Semester, semester: Semester,
grades: List<Pair<String, List<Grade>>>, grades: List<Pair<String, List<Grade>>>,
calcAverage: Boolean calcAverage: Boolean,
params: AverageCalcParams,
): List<GradeSummary> { ): List<GradeSummary> {
if (isNotEmpty() && size > grades.size) return this if (isNotEmpty() && size > grades.size) return this
@ -211,15 +239,16 @@ class GradeAverageProvider @Inject constructor(
proposedPoints = "", proposedPoints = "",
finalPoints = "", finalPoints = "",
pointsSum = "", pointsSum = "",
average = if (calcAverage) details.updateModifiers(student) average = if (calcAverage) details.updateModifiers(student, params)
.calcAverage(isOptionalArithmeticAverage) else .0 .calcAverage(params.isOptionalArithmeticAverage) else .0
) )
} }
} }
private fun List<Grade>.updateModifiers(student: Student): List<Grade> { private fun List<Grade>.updateModifiers(
return if (student.loginMode == Sdk.Mode.SCRAPPER.name) { student: Student,
map { it.changeModifier(plusModifier, minusModifier) } params: AverageCalcParams,
} else this ): List<Grade> = if (student.loginMode == Sdk.Mode.SCRAPPER.name) {
} map { it.changeModifier(params.plusModifier, params.minusModifier) }
} else this
} }

View File

@ -8,6 +8,7 @@ import android.view.View
import android.view.View.INVISIBLE import android.view.View.INVISIBLE
import android.view.View.VISIBLE import android.view.View.VISIBLE
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R import io.github.wulkanowy.R
@ -141,7 +142,7 @@ class GradeFragment : BaseFragment<FragmentGradeBinding>(R.layout.fragment_grade
val choices = semesters.map { getString(R.string.grade_semester, it.semesterName) } val choices = semesters.map { getString(R.string.grade_semester, it.semesterName) }
.toTypedArray() .toTypedArray()
AlertDialog.Builder(requireContext()) MaterialAlertDialogBuilder(requireContext())
.setSingleChoiceItems(choices, selectedIndex) { dialog, which -> .setSingleChoiceItems(choices, selectedIndex) { dialog, which ->
presenter.onSemesterSelected(which) presenter.onSemesterSelected(which)
dialog.dismiss() dialog.dismiss()

View File

@ -1,6 +1,7 @@
package io.github.wulkanowy.ui.modules.grade.details package io.github.wulkanowy.ui.modules.grade.details
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.res.ColorStateList
import android.content.res.Resources import android.content.res.Resources
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
@ -17,9 +18,10 @@ import io.github.wulkanowy.databinding.HeaderGradeDetailsBinding
import io.github.wulkanowy.databinding.ItemGradeDetailsBinding import io.github.wulkanowy.databinding.ItemGradeDetailsBinding
import io.github.wulkanowy.ui.base.BaseExpandableAdapter import io.github.wulkanowy.ui.base.BaseExpandableAdapter
import io.github.wulkanowy.utils.getBackgroundColor import io.github.wulkanowy.utils.getBackgroundColor
import io.github.wulkanowy.utils.getCompatColor
import io.github.wulkanowy.utils.toFormattedString import io.github.wulkanowy.utils.toFormattedString
import timber.log.Timber import timber.log.Timber
import java.util.BitSet import java.util.*
import javax.inject.Inject import javax.inject.Inject
class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter<RecyclerView.ViewHolder>() { class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter<RecyclerView.ViewHolder>() {
@ -203,7 +205,9 @@ class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter<Recycler
with(holder.binding) { with(holder.binding) {
gradeItemValue.run { gradeItemValue.run {
text = grade.entry text = grade.entry
setBackgroundResource(grade.getBackgroundColor(gradeColorTheme)) backgroundTintList = ColorStateList.valueOf(
context.getCompatColor(grade.getBackgroundColor(gradeColorTheme))
)
} }
gradeItemDescription.text = when { gradeItemDescription.text = when {
grade.description.isNotBlank() -> grade.description grade.description.isNotBlank() -> grade.description

View File

@ -1,22 +1,23 @@
package io.github.wulkanowy.ui.modules.grade.details package io.github.wulkanowy.ui.modules.grade.details
import android.app.Dialog
import android.content.res.ColorStateList
import android.os.Bundle import android.os.Bundle
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 androidx.core.content.ContextCompat
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.fragment.app.DialogFragment import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
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
import io.github.wulkanowy.data.enums.GradeColorTheme import io.github.wulkanowy.data.enums.GradeColorTheme
import io.github.wulkanowy.databinding.DialogGradeBinding import io.github.wulkanowy.databinding.DialogGradeBinding
import io.github.wulkanowy.ui.base.BaseDialogFragment
import io.github.wulkanowy.utils.* import io.github.wulkanowy.utils.*
@AndroidEntryPoint
class GradeDetailsDialog : DialogFragment() { class GradeDetailsDialog : BaseDialogFragment<DialogGradeBinding>() {
private var binding: DialogGradeBinding by lifecycleAwareVariable()
private lateinit var grade: Grade private lateinit var grade: Grade
@ -38,16 +39,15 @@ class GradeDetailsDialog : DialogFragment() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setStyle(STYLE_NO_TITLE, 0)
grade = requireArguments().serializable(ARGUMENT_KEY) grade = requireArguments().serializable(ARGUMENT_KEY)
gradeColorTheme = requireArguments().serializable(COLOR_THEME_KEY) gradeColorTheme = requireArguments().serializable(COLOR_THEME_KEY)
} }
override fun onCreateView( override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
inflater: LayoutInflater, return MaterialAlertDialogBuilder(requireContext(), theme)
container: ViewGroup?, .setView(DialogGradeBinding.inflate(layoutInflater).apply { binding = this }.root)
savedInstanceState: Bundle? .create()
) = DialogGradeBinding.inflate(inflater).apply { binding = this }.root }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
@ -55,10 +55,9 @@ class GradeDetailsDialog : DialogFragment() {
with(binding) { with(binding) {
gradeDialogSubject.text = grade.subject gradeDialogSubject.text = grade.subject
gradeDialogColorAndWeightValue.run { gradeDialogWeightValue.text = grade.weight
text = context.getString(R.string.grade_weight_value, grade.weight) gradeDialogWeightLayout.backgroundTintList =
setBackgroundResource(grade.getGradeColor()) ColorStateList.valueOf(requireContext().getCompatColor(grade.getGradeColor()))
}
gradeDialogDateValue.text = grade.date.toFormattedString() gradeDialogDateValue.text = grade.date.toFormattedString()
gradeDialogColorValue.text = getString(grade.colorStringId) gradeDialogColorValue.text = getString(grade.colorStringId)
@ -72,7 +71,12 @@ class GradeDetailsDialog : DialogFragment() {
gradeDialogValue.run { gradeDialogValue.run {
text = grade.entry text = grade.entry
setBackgroundResource(grade.getBackgroundColor(gradeColorTheme)) backgroundTintList = ColorStateList.valueOf(
ContextCompat.getColor(
requireContext(),
grade.getBackgroundColor(gradeColorTheme)
)
)
} }
gradeDialogTeacherValue.text = grade.teacher.ifBlank { getString(R.string.all_no_data) } gradeDialogTeacherValue.text = grade.teacher.ifBlank { getString(R.string.all_no_data) }

View File

@ -116,7 +116,9 @@ class GradeStatisticsAdapter @Inject constructor() :
} }
) )
binding.gradeStatisticsTypeSwitch.setOnCheckedChangeListener { _, checkedId -> binding.gradeStatisticsTypeSwitch.addOnButtonCheckedListener { _, checkedId, isChecked ->
if (!isChecked) return@addOnButtonCheckedListener
currentDataType = when (checkedId) { currentDataType = when (checkedId) {
R.id.gradeStatisticsTypePartial -> GradeStatisticsItem.DataType.PARTIAL R.id.gradeStatisticsTypePartial -> GradeStatisticsItem.DataType.PARTIAL
R.id.gradeStatisticsTypeSemester -> GradeStatisticsItem.DataType.SEMESTER R.id.gradeStatisticsTypeSemester -> GradeStatisticsItem.DataType.SEMESTER

View File

@ -7,6 +7,7 @@ import android.view.View.INVISIBLE
import android.view.View.VISIBLE import android.view.View.VISIBLE
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
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.GradeSummary import io.github.wulkanowy.data.db.entities.GradeSummary
@ -118,7 +119,7 @@ class GradeSummaryFragment :
} }
override fun showCalculatedAverageHelpDialog() { override fun showCalculatedAverageHelpDialog() {
AlertDialog.Builder(requireContext()) MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.grade_summary_calculated_average_help_dialog_title) .setTitle(R.string.grade_summary_calculated_average_help_dialog_title)
.setMessage(R.string.grade_summary_calculated_average_help_dialog_message) .setMessage(R.string.grade_summary_calculated_average_help_dialog_message)
.setPositiveButton(R.string.all_close) { _, _ -> } .setPositiveButton(R.string.all_close) { _, _ -> }
@ -126,7 +127,7 @@ class GradeSummaryFragment :
} }
override fun showFinalAverageHelpDialog() { override fun showFinalAverageHelpDialog() {
AlertDialog.Builder(requireContext()) MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.grade_summary_final_average_help_dialog_title) .setTitle(R.string.grade_summary_final_average_help_dialog_title)
.setMessage(R.string.grade_summary_final_average_help_dialog_message) .setMessage(R.string.grade_summary_final_average_help_dialog_message)
.setPositiveButton(R.string.all_close) { _, _ -> } .setPositiveButton(R.string.all_close) { _, _ -> }

View File

@ -21,7 +21,7 @@ import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class HomeworkFragment : BaseFragment<FragmentHomeworkBinding>(R.layout.fragment_homework), class HomeworkFragment : BaseFragment<FragmentHomeworkBinding>(R.layout.fragment_homework),
HomeworkView, MainView.TitledView { HomeworkView, MainView.TitledView, MainView.MainChildView {
@Inject @Inject
lateinit var presenter: HomeworkPresenter lateinit var presenter: HomeworkPresenter
@ -67,7 +67,7 @@ class HomeworkFragment : BaseFragment<FragmentHomeworkBinding>(R.layout.fragment
openAddHomeworkButton.setOnClickListener { presenter.onHomeworkAddButtonClicked() } openAddHomeworkButton.setOnClickListener { presenter.onHomeworkAddButtonClicked() }
homeworkNavContainer.elevation = requireContext().dpToPx(8f) homeworkNavContainer.elevation = requireContext().dpToPx(3f)
} }
} }
@ -133,6 +133,14 @@ class HomeworkFragment : BaseFragment<FragmentHomeworkBinding>(R.layout.fragment
(activity as? MainActivity)?.showDialogFragment(HomeworkAddDialog()) (activity as? MainActivity)?.showDialogFragment(HomeworkAddDialog())
} }
override fun onFragmentReselected() {
if (::presenter.isInitialized) presenter.onViewReselected()
}
override fun resetView() {
binding.homeworkRecycler.smoothScrollToPosition(0)
}
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
outState.putLong(SAVED_DATE_KEY, presenter.currentDate.toEpochDay()) outState.putLong(SAVED_DATE_KEY, presenter.currentDate.toEpochDay())

View File

@ -177,8 +177,21 @@ class HomeworkPresenter @Inject constructor(
showNextButton(!currentDate.plusDays(7).isHolidays) showNextButton(!currentDate.plusDays(7).isHolidays)
updateNavigationWeek( updateNavigationWeek(
"${currentDate.monday.toFormattedString("dd.MM")} - " + "${currentDate.monday.toFormattedString("dd.MM")} - " +
currentDate.sunday.toFormattedString("dd.MM") currentDate.sunday.toFormattedString("dd.MM")
) )
} }
} }
fun onViewReselected() {
Timber.i("Homework view is reselected")
baseDate = LocalDate.now().nextOrSameSchoolDay
if (currentDate != baseDate) {
reloadView(baseDate)
loadData()
} else if (view?.isViewEmpty == false) {
view?.resetView()
}
}
} }

View File

@ -36,4 +36,6 @@ interface HomeworkView : BaseView {
fun showHomeworkDialog(homework: Homework) fun showHomeworkDialog(homework: Homework)
fun showAddHomeworkDialog() fun showAddHomeworkDialog()
fun resetView()
} }

View File

@ -1,10 +1,10 @@
package io.github.wulkanowy.ui.modules.homework.add package io.github.wulkanowy.ui.modules.homework.add
import android.app.Dialog
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.core.widget.doOnTextChanged import androidx.core.widget.doOnTextChanged
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.databinding.DialogHomeworkAddBinding import io.github.wulkanowy.databinding.DialogHomeworkAddBinding
@ -21,20 +21,15 @@ class HomeworkAddDialog : BaseDialogFragment<DialogHomeworkAddBinding>(), Homewo
@Inject @Inject
lateinit var presenter: HomeworkAddPresenter lateinit var presenter: HomeworkAddPresenter
// todo: move it to presenter //todo: move it to presenter
private var date: LocalDate? = null private var date: LocalDate? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
super.onCreate(savedInstanceState) return MaterialAlertDialogBuilder(requireContext(), theme)
setStyle(STYLE_NO_TITLE, 0) .setView(DialogHomeworkAddBinding.inflate(layoutInflater).apply { binding = this }.root)
.create()
} }
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
) = DialogHomeworkAddBinding.inflate(inflater).apply { binding = this }.root
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) presenter.onAttachView(this)

View File

@ -31,14 +31,8 @@ class HomeworkDetailsAdapter @Inject constructor() :
attachments = value?.attachments.orEmpty() attachments = value?.attachments.orEmpty()
} }
var isHomeworkFullscreen = false
var onAttachmentClickListener: (url: String) -> Unit = {} var onAttachmentClickListener: (url: String) -> Unit = {}
var onFullScreenClickListener = {}
var onFullScreenExitClickListener = {}
var onDeleteClickListener: (homework: Homework) -> Unit = {} var onDeleteClickListener: (homework: Homework) -> Unit = {}
override fun getItemCount() = 1 + if (attachments.isNotEmpty()) attachments.size + 1 else 0 override fun getItemCount() = 1 + if (attachments.isNotEmpty()) attachments.size + 1 else 0
@ -82,18 +76,6 @@ class HomeworkDetailsAdapter @Inject constructor() :
homeworkDialogTeacher.text = homework?.teacher.ifNullOrBlank { noDataString } homeworkDialogTeacher.text = homework?.teacher.ifNullOrBlank { noDataString }
homeworkDialogContent.text = homework?.content.ifNullOrBlank { noDataString } homeworkDialogContent.text = homework?.content.ifNullOrBlank { noDataString }
homeworkDialogDelete.visibility = if (homework?.isAddedByUser == true) VISIBLE else GONE homeworkDialogDelete.visibility = if (homework?.isAddedByUser == true) VISIBLE else GONE
homeworkDialogFullScreen.visibility = if (isHomeworkFullscreen) GONE else VISIBLE
homeworkDialogFullScreenExit.visibility = if (isHomeworkFullscreen) VISIBLE else GONE
homeworkDialogFullScreen.setOnClickListener {
homeworkDialogFullScreen.visibility = GONE
homeworkDialogFullScreenExit.visibility = VISIBLE
onFullScreenClickListener()
}
homeworkDialogFullScreenExit.setOnClickListener {
homeworkDialogFullScreen.visibility = VISIBLE
homeworkDialogFullScreenExit.visibility = GONE
onFullScreenExitClickListener()
}
homeworkDialogDelete.setOnClickListener { homeworkDialogDelete.setOnClickListener {
onDeleteClickListener(homework!!) onDeleteClickListener(homework!!)
} }

View File

@ -1,14 +1,12 @@
package io.github.wulkanowy.ui.modules.homework.details package io.github.wulkanowy.ui.modules.homework.details
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Dialog
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
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.Homework import io.github.wulkanowy.data.db.entities.Homework
@ -43,15 +41,14 @@ class HomeworkDetailsDialog : BaseDialogFragment<DialogHomeworkBinding>(), Homew
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setStyle(STYLE_NO_TITLE, 0)
homework = requireArguments().serializable(ARGUMENT_KEY) homework = requireArguments().serializable(ARGUMENT_KEY)
} }
override fun onCreateView( override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
inflater: LayoutInflater, return MaterialAlertDialogBuilder(requireContext(), theme)
container: ViewGroup?, .setView(DialogHomeworkBinding.inflate(layoutInflater).apply { binding = this }.root)
savedInstanceState: Bundle? .create()
) = DialogHomeworkBinding.inflate(inflater).apply { binding = this }.root }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
@ -67,26 +64,11 @@ class HomeworkDetailsDialog : BaseDialogFragment<DialogHomeworkBinding>(), Homew
homeworkDialogClose.setOnClickListener { dismiss() } homeworkDialogClose.setOnClickListener { dismiss() }
} }
if (presenter.isHomeworkFullscreen) {
dialog?.window?.setLayout(MATCH_PARENT, MATCH_PARENT)
} else {
dialog?.window?.setLayout(WRAP_CONTENT, WRAP_CONTENT)
}
with(binding.homeworkDialogRecycler) { with(binding.homeworkDialogRecycler) {
layoutManager = LinearLayoutManager(context) layoutManager = LinearLayoutManager(context)
adapter = detailsAdapter.apply { adapter = detailsAdapter.apply {
onAttachmentClickListener = { context.openInternetBrowser(it, ::showMessage) } onAttachmentClickListener = { context.openInternetBrowser(it, ::showMessage) }
onFullScreenClickListener = {
dialog?.window?.setLayout(MATCH_PARENT, MATCH_PARENT)
presenter.isHomeworkFullscreen = true
}
onFullScreenExitClickListener = {
dialog?.window?.setLayout(WRAP_CONTENT, WRAP_CONTENT)
presenter.isHomeworkFullscreen = false
}
onDeleteClickListener = { homework -> presenter.deleteHomework(homework) } onDeleteClickListener = { homework -> presenter.deleteHomework(homework) }
isHomeworkFullscreen = presenter.isHomeworkFullscreen
homework = this@HomeworkDetailsDialog.homework homework = this@HomeworkDetailsDialog.homework
} }
} }

View File

@ -5,7 +5,6 @@ import io.github.wulkanowy.data.logResourceStatus
import io.github.wulkanowy.data.onResourceError import io.github.wulkanowy.data.onResourceError
import io.github.wulkanowy.data.onResourceSuccess import io.github.wulkanowy.data.onResourceSuccess
import io.github.wulkanowy.data.repositories.HomeworkRepository import io.github.wulkanowy.data.repositories.HomeworkRepository
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.resourceFlow import io.github.wulkanowy.data.resourceFlow
import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.BasePresenter
@ -19,15 +18,8 @@ class HomeworkDetailsPresenter @Inject constructor(
studentRepository: StudentRepository, studentRepository: StudentRepository,
private val homeworkRepository: HomeworkRepository, private val homeworkRepository: HomeworkRepository,
private val analytics: AnalyticsHelper, private val analytics: AnalyticsHelper,
private val preferencesRepository: PreferencesRepository
) : BasePresenter<HomeworkDetailsView>(errorHandler, studentRepository) { ) : BasePresenter<HomeworkDetailsView>(errorHandler, studentRepository) {
var isHomeworkFullscreen
get() = preferencesRepository.isHomeworkFullscreen
set(value) {
preferencesRepository.isHomeworkFullscreen = value
}
override fun onAttachView(view: HomeworkDetailsView) { override fun onAttachView(view: HomeworkDetailsView) {
super.onAttachView(view) super.onAttachView(view)
view.initView() view.initView()

View File

@ -4,13 +4,15 @@ import android.content.Context
import android.database.sqlite.SQLiteConstraintException import android.database.sqlite.SQLiteConstraintException
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.sdk.mobile.exception.InvalidPinException import io.github.wulkanowy.sdk.hebe.exception.InvalidPinException
import io.github.wulkanowy.sdk.mobile.exception.InvalidSymbolException import io.github.wulkanowy.sdk.hebe.exception.InvalidTokenException
import io.github.wulkanowy.sdk.mobile.exception.InvalidTokenException import io.github.wulkanowy.sdk.hebe.exception.TokenDeadException
import io.github.wulkanowy.sdk.mobile.exception.TokenDeadException import io.github.wulkanowy.sdk.hebe.exception.UnknownTokenException
import io.github.wulkanowy.sdk.scrapper.login.BadCredentialsException import io.github.wulkanowy.sdk.scrapper.login.BadCredentialsException
import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.ui.base.ErrorHandler
import javax.inject.Inject import javax.inject.Inject
import io.github.wulkanowy.sdk.hebe.exception.InvalidSymbolException as InvalidHebeSymbolException
import io.github.wulkanowy.sdk.scrapper.login.InvalidSymbolException as InvalidScrapperSymbolException
class LoginErrorHandler @Inject constructor( class LoginErrorHandler @Inject constructor(
@ApplicationContext context: Context, @ApplicationContext context: Context,
@ -32,9 +34,11 @@ class LoginErrorHandler @Inject constructor(
is BadCredentialsException -> onBadCredentials(error.message) is BadCredentialsException -> onBadCredentials(error.message)
is SQLiteConstraintException -> onStudentDuplicate(resources.getString(R.string.login_duplicate_student)) is SQLiteConstraintException -> onStudentDuplicate(resources.getString(R.string.login_duplicate_student))
is TokenDeadException -> onInvalidToken(resources.getString(R.string.login_expired_token)) is TokenDeadException -> onInvalidToken(resources.getString(R.string.login_expired_token))
is UnknownTokenException,
is InvalidTokenException -> onInvalidToken(resources.getString(R.string.login_invalid_token)) is InvalidTokenException -> onInvalidToken(resources.getString(R.string.login_invalid_token))
is InvalidPinException -> onInvalidPin(resources.getString(R.string.login_invalid_pin)) is InvalidPinException -> onInvalidPin(resources.getString(R.string.login_invalid_pin))
is InvalidSymbolException -> onInvalidSymbol(resources.getString(R.string.login_invalid_symbol)) is InvalidScrapperSymbolException,
is InvalidHebeSymbolException -> onInvalidSymbol(resources.getString(R.string.login_invalid_symbol))
else -> super.proceed(error) else -> super.proceed(error)
} }
} }

View File

@ -34,9 +34,9 @@ class LoginAdvancedFragment :
override val formLoginType: String override val formLoginType: String
get() = when (binding.loginTypeSwitch.checkedRadioButtonId) { get() = when (binding.loginTypeSwitch.checkedRadioButtonId) {
R.id.loginTypeApi -> "API" R.id.loginTypeApi -> Sdk.Mode.HEBE.name
R.id.loginTypeScrapper -> "SCRAPPER" R.id.loginTypeScrapper -> Sdk.Mode.SCRAPPER.name
else -> "HYBRID" else -> Sdk.Mode.HYBRID.name
} }
override val formUsernameValue: String override val formUsernameValue: String
@ -99,7 +99,7 @@ class LoginAdvancedFragment :
loginTypeSwitch.setOnCheckedChangeListener { _, checkedId -> loginTypeSwitch.setOnCheckedChangeListener { _, checkedId ->
presenter.onLoginModeSelected( presenter.onLoginModeSelected(
when (checkedId) { when (checkedId) {
R.id.loginTypeApi -> Sdk.Mode.API R.id.loginTypeApi -> Sdk.Mode.HEBE
R.id.loginTypeScrapper -> Sdk.Mode.SCRAPPER R.id.loginTypeScrapper -> Sdk.Mode.SCRAPPER
else -> Sdk.Mode.HYBRID else -> Sdk.Mode.HYBRID
} }

View File

@ -1,17 +1,12 @@
package io.github.wulkanowy.ui.modules.login.advanced package io.github.wulkanowy.ui.modules.login.advanced
import io.github.wulkanowy.data.Resource import io.github.wulkanowy.data.Resource
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.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.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
@ -97,14 +92,16 @@ class LoginAdvancedPresenter @Inject constructor(
fun onLoginModeSelected(type: Sdk.Mode) { fun onLoginModeSelected(type: Sdk.Mode) {
view?.run { view?.run {
when (type) { when (type) {
Sdk.Mode.API -> { Sdk.Mode.HEBE -> {
showOnlyMobileApiModeInputs() showOnlyMobileApiModeInputs()
showMobileApiWarningMessage() showMobileApiWarningMessage()
} }
Sdk.Mode.SCRAPPER -> { Sdk.Mode.SCRAPPER -> {
showOnlyScrapperModeInputs() showOnlyScrapperModeInputs()
showScraperWarningMessage() showScraperWarningMessage()
} }
Sdk.Mode.HYBRID -> { Sdk.Mode.HYBRID -> {
showOnlyHybridModeInputs() showOnlyHybridModeInputs()
showHybridWarningMessage() showHybridWarningMessage()
@ -145,11 +142,12 @@ class LoginAdvancedPresenter @Inject constructor(
showProgress(true) showProgress(true)
showContent(false) showContent(false)
} }
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, "scrapperBaseUrl" to view?.formHostValue.orEmpty(),
"error" to "No error" "error" to "No error"
) )
val loginData = LoginData( val loginData = LoginData(
@ -158,14 +156,15 @@ class LoginAdvancedPresenter @Inject constructor(
baseUrl = view?.formHostValue.orEmpty().trim(), baseUrl = view?.formHostValue.orEmpty().trim(),
symbol = view?.formSymbolValue.orEmpty().trim().getNormalizedSymbol(), symbol = view?.formSymbolValue.orEmpty().trim().getNormalizedSymbol(),
) )
when (it.data.size) { when (it.data.symbols.size) {
0 -> view?.navigateToSymbol(loginData) 0 -> view?.navigateToSymbol(loginData)
else -> view?.navigateToStudentSelect( else -> view?.navigateToStudentSelect(
loginData = loginData, loginData = loginData,
registerUser = it.data.toRegisterUser(loginData), registerUser = it.data,
) )
} }
} }
is Resource.Error -> { is Resource.Error -> {
analytics.logEvent( analytics.logEvent(
"registration_form", "registration_form",
@ -183,59 +182,7 @@ class LoginAdvancedPresenter @Inject constructor(
}.launch("login") }.launch("login")
} }
private fun List<StudentWithSemesters>.toRegisterUser(loginData: LoginData) = RegisterUser( private suspend fun getStudentsAppropriatesToLoginType(): 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> {
val email = view?.formUsernameValue.orEmpty() val email = view?.formUsernameValue.orEmpty()
val password = view?.formPassValue.orEmpty() val password = view?.formPassValue.orEmpty()
val endpoint = view?.formHostValue.orEmpty() val endpoint = view?.formHostValue.orEmpty()
@ -245,10 +192,11 @@ class LoginAdvancedPresenter @Inject constructor(
val token = view?.formTokenValue.orEmpty() val token = view?.formTokenValue.orEmpty()
return when (Sdk.Mode.valueOf(view?.formLoginType.orEmpty())) { return when (Sdk.Mode.valueOf(view?.formLoginType.orEmpty())) {
Sdk.Mode.API -> studentRepository.getStudentsApi(pin, symbol, token) Sdk.Mode.HEBE -> studentRepository.getStudentsApi(pin, symbol, token)
Sdk.Mode.SCRAPPER -> studentRepository.getStudentsScrapper( Sdk.Mode.SCRAPPER -> studentRepository.getStudentsScrapper(
email, password, endpoint, symbol email, password, endpoint, symbol
) )
Sdk.Mode.HYBRID -> studentRepository.getStudentsHybrid( Sdk.Mode.HYBRID -> studentRepository.getStudentsHybrid(
email, password, endpoint, symbol email, password, endpoint, symbol
) )
@ -267,8 +215,8 @@ class LoginAdvancedPresenter @Inject constructor(
var isCorrect = true var isCorrect = true
when (Sdk.Mode.valueOf(view?.formLoginType ?: "")) { when (Sdk.Mode.valueOf(view?.formLoginType.orEmpty())) {
Sdk.Mode.API -> { Sdk.Mode.HEBE -> {
if (pin.isEmpty()) { if (pin.isEmpty()) {
view?.setErrorPinRequired() view?.setErrorPinRequired()
isCorrect = false isCorrect = false
@ -284,17 +232,17 @@ class LoginAdvancedPresenter @Inject constructor(
isCorrect = false isCorrect = false
} }
} }
Sdk.Mode.HYBRID, Sdk.Mode.SCRAPPER -> { Sdk.Mode.HYBRID, Sdk.Mode.SCRAPPER -> {
if (login.isEmpty()) { if (login.isEmpty()) {
view?.setErrorUsernameRequired() view?.setErrorUsernameRequired()
isCorrect = false isCorrect = false
} else { } else {
if ("@" in login && "standard" !in host) { if ("@" in login && "login" in host) {
view?.setErrorLoginRequired() view?.setErrorLoginRequired()
isCorrect = false isCorrect = false
} }
if ("@" !in login && "email" in host) {
if ("@" !in login && "standard" in host) {
view?.setErrorEmailRequired() view?.setErrorEmailRequired()
isCorrect = false isCorrect = false
} }

View File

@ -1,6 +1,5 @@
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.pojos.RegisterUser import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.ui.base.BaseView import io.github.wulkanowy.ui.base.BaseView
import io.github.wulkanowy.ui.modules.login.LoginData import io.github.wulkanowy.ui.modules.login.LoginData

View File

@ -15,12 +15,7 @@ import io.github.wulkanowy.databinding.FragmentLoginFormBinding
import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.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.setOnEditorDoneSignIn
import io.github.wulkanowy.utils.showSoftInput
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
@ -149,12 +144,14 @@ class LoginFormFragment : BaseFragment<FragmentLoginFormBinding>(R.layout.fragme
override fun setErrorPassRequired(focus: Boolean) { override fun setErrorPassRequired(focus: Boolean) {
with(binding.loginFormPassLayout) { with(binding.loginFormPassLayout) {
error = getString(R.string.error_field_required) error = getString(R.string.error_field_required)
setEndIconTintList(requireContext().getAttrColorStateList(R.attr.colorError))
} }
} }
override fun setErrorPassInvalid(focus: Boolean) { override fun setErrorPassInvalid(focus: Boolean) {
with(binding.loginFormPassLayout) { with(binding.loginFormPassLayout) {
error = getString(R.string.login_invalid_password) error = getString(R.string.login_invalid_password)
setEndIconTintList(requireContext().getAttrColorStateList(R.attr.colorError))
} }
} }
@ -162,6 +159,7 @@ class LoginFormFragment : BaseFragment<FragmentLoginFormBinding>(R.layout.fragme
with(binding) { with(binding) {
loginFormUsernameLayout.error = " " loginFormUsernameLayout.error = " "
loginFormPassLayout.error = " " loginFormPassLayout.error = " "
loginFormPassLayout.setEndIconTintList(requireContext().getAttrColorStateList(R.attr.colorError))
loginFormHostLayout.error = " " loginFormHostLayout.error = " "
loginFormErrorBox.text = message ?: getString(R.string.login_incorrect_password_default) loginFormErrorBox.text = message ?: getString(R.string.login_incorrect_password_default)
loginFormErrorBox.isVisible = true loginFormErrorBox.isVisible = true
@ -181,6 +179,7 @@ class LoginFormFragment : BaseFragment<FragmentLoginFormBinding>(R.layout.fragme
override fun clearPassError() { override fun clearPassError() {
binding.loginFormPassLayout.error = null binding.loginFormPassLayout.error = null
binding.loginFormPassLayout.setEndIconTintList(null)
binding.loginFormErrorBox.isVisible = false binding.loginFormErrorBox.isVisible = false
} }
@ -205,6 +204,10 @@ class LoginFormFragment : BaseFragment<FragmentLoginFormBinding>(R.layout.fragme
binding.loginFormContainer.visibility = if (show) VISIBLE else GONE binding.loginFormContainer.visibility = if (show) VISIBLE else GONE
} }
override fun showOtherOptionsButton(show: Boolean) {
binding.loginFormAdvancedButton.isVisible = show
}
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
override fun showVersion() { override fun showVersion() {
binding.loginFormVersion.text = "v${appInfo.versionName}" binding.loginFormVersion.text = "v${appInfo.versionName}"

View File

@ -7,6 +7,7 @@ import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.modules.login.LoginData import io.github.wulkanowy.ui.modules.login.LoginData
import io.github.wulkanowy.ui.modules.login.LoginErrorHandler import io.github.wulkanowy.ui.modules.login.LoginErrorHandler
import io.github.wulkanowy.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 timber.log.Timber import timber.log.Timber
import java.net.URL import java.net.URL
@ -15,6 +16,7 @@ import javax.inject.Inject
class LoginFormPresenter @Inject constructor( class LoginFormPresenter @Inject constructor(
studentRepository: StudentRepository, studentRepository: StudentRepository,
private val loginErrorHandler: LoginErrorHandler, private val loginErrorHandler: LoginErrorHandler,
private val appInfo: AppInfo,
private val analytics: AnalyticsHelper private val analytics: AnalyticsHelper
) : BasePresenter<LoginFormView>(loginErrorHandler, studentRepository) { ) : BasePresenter<LoginFormView>(loginErrorHandler, studentRepository) {
@ -25,6 +27,7 @@ class LoginFormPresenter @Inject constructor(
view.run { view.run {
initView() initView()
showContact(false) showContact(false)
showOtherOptionsButton(appInfo.isDebug)
showVersion() showVersion()
loginErrorHandler.onBadCredentials = { loginErrorHandler.onBadCredentials = {

View File

@ -56,6 +56,8 @@ interface LoginFormView : BaseView {
fun showContent(show: Boolean) fun showContent(show: Boolean)
fun showOtherOptionsButton(show: Boolean)
fun showVersion() fun showVersion()
fun navigateToSymbol(loginData: LoginData) fun navigateToSymbol(loginData: LoginData)

View File

@ -55,7 +55,6 @@ class LoginStudentSelectFragment :
} }
} }
@Suppress("UNCHECKED_CAST")
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)

View File

@ -18,7 +18,7 @@ import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class LuckyNumberFragment : class LuckyNumberFragment :
BaseFragment<FragmentLuckyNumberBinding>(R.layout.fragment_lucky_number), LuckyNumberView, BaseFragment<FragmentLuckyNumberBinding>(R.layout.fragment_lucky_number), LuckyNumberView,
MainView.TitledView { MainView.TitledView, MainView.MainChildView {
@Inject @Inject
lateinit var presenter: LuckyNumberPresenter lateinit var presenter: LuckyNumberPresenter
@ -86,6 +86,14 @@ class LuckyNumberFragment :
(activity as? MainActivity)?.pushView(LuckyNumberHistoryFragment.newInstance()) (activity as? MainActivity)?.pushView(LuckyNumberHistoryFragment.newInstance())
} }
override fun onFragmentReselected() {
if (::presenter.isInitialized) presenter.onViewReselected()
}
override fun popView() {
(activity as? MainActivity)?.popView()
}
override fun onDestroyView() { override fun onDestroyView() {
presenter.onDetachView() presenter.onDetachView()
super.onDestroyView() super.onDestroyView()

View File

@ -99,4 +99,9 @@ class LuckyNumberPresenter @Inject constructor(
fun onDetailsClick() { fun onDetailsClick() {
view?.showErrorDetailsDialog(lastError) view?.showErrorDetailsDialog(lastError)
} }
fun onViewReselected() {
Timber.i("Luckynumber view is reselected")
view?.popView()
}
} }

View File

@ -26,4 +26,6 @@ interface LuckyNumberView : BaseView {
fun showContent(show: Boolean) fun showContent(show: Boolean)
fun openLuckyNumberHistory() fun openLuckyNumberHistory()
fun popView()
} }

View File

@ -61,7 +61,7 @@ class LuckyNumberHistoryFragment :
luckyNumberHistoryPreviousButton.setOnClickListener { presenter.onPreviousWeek() } luckyNumberHistoryPreviousButton.setOnClickListener { presenter.onPreviousWeek() }
luckyNumberHistoryNextButton.setOnClickListener { presenter.onNextWeek() } luckyNumberHistoryNextButton.setOnClickListener { presenter.onNextWeek() }
luckyNumberHistoryNavContainer.elevation = requireContext().dpToPx(8f) luckyNumberHistoryNavContainer.elevation = requireContext().dpToPx(3f)
} }
} }

View File

@ -1,16 +1,12 @@
package io.github.wulkanowy.ui.modules.luckynumberwidget package io.github.wulkanowy.ui.modules.luckynumberwidget
import android.appwidget.AppWidgetManager.ACTION_APPWIDGET_UPDATE import android.appwidget.AppWidgetManager.*
import android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_ID
import android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_IDS
import android.content.Intent import android.content.Intent
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
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.data.db.entities.StudentWithSemesters import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.databinding.ActivityWidgetConfigureBinding import io.github.wulkanowy.databinding.ActivityWidgetConfigureBinding
import io.github.wulkanowy.ui.base.BaseActivity import io.github.wulkanowy.ui.base.BaseActivity
@ -41,7 +37,6 @@ class LuckyNumberWidgetConfigureActivity :
setContentView( setContentView(
ActivityWidgetConfigureBinding.inflate(layoutInflater).apply { binding = this }.root ActivityWidgetConfigureBinding.inflate(layoutInflater).apply { binding = this }.root
) )
intent.extras.let { intent.extras.let {
presenter.onAttachView(this, it?.getInt(EXTRA_APPWIDGET_ID)) presenter.onAttachView(this, it?.getInt(EXTRA_APPWIDGET_ID))
} }
@ -56,22 +51,6 @@ class LuckyNumberWidgetConfigureActivity :
configureAdapter.onClickListener = presenter::onItemSelect configureAdapter.onClickListener = presenter::onItemSelect
} }
override fun showThemeDialog() {
var items = arrayOf(
getString(R.string.widget_timetable_theme_light),
getString(R.string.widget_timetable_theme_dark)
)
if (appInfo.systemVersion >= Build.VERSION_CODES.Q) items += (getString(R.string.widget_timetable_theme_system))
dialog = AlertDialog.Builder(this, R.style.WulkanowyTheme_WidgetAccountSwitcher)
.setTitle(R.string.widget_timetable_theme_title)
.setOnDismissListener { presenter.onDismissThemeView() }
.setSingleChoiceItems(items, -1) { _, which ->
presenter.onThemeSelect(which)
}
.show()
}
override fun updateData(data: List<StudentWithSemesters>, selectedStudentId: Long) { override fun updateData(data: List<StudentWithSemesters>, selectedStudentId: Long) {
with(configureAdapter) { with(configureAdapter) {
selectedId = selectedStudentId selectedId = selectedStudentId

View File

@ -8,7 +8,6 @@ import io.github.wulkanowy.data.resourceFlow
import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.ui.modules.luckynumberwidget.LuckyNumberWidgetProvider.Companion.getStudentWidgetKey import io.github.wulkanowy.ui.modules.luckynumberwidget.LuckyNumberWidgetProvider.Companion.getStudentWidgetKey
import io.github.wulkanowy.ui.modules.luckynumberwidget.LuckyNumberWidgetProvider.Companion.getThemeWidgetKey
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -32,20 +31,9 @@ class LuckyNumberWidgetConfigurePresenter @Inject constructor(
fun onItemSelect(student: Student) { fun onItemSelect(student: Student) {
selectedStudent = student selectedStudent = student
view?.showThemeDialog()
}
fun onThemeSelect(index: Int) {
appWidgetId?.let {
sharedPref.putLong(getThemeWidgetKey(it), index.toLong())
}
registerStudent(selectedStudent) registerStudent(selectedStudent)
} }
fun onDismissThemeView() {
view?.finishView()
}
private fun loadData() { private fun loadData() {
resourceFlow { studentRepository.getSavedStudents(false) }.onEach { resourceFlow { studentRepository.getSavedStudents(false) }.onEach {
when (it) { when (it) {
@ -56,10 +44,7 @@ class LuckyNumberWidgetConfigurePresenter @Inject constructor(
} ?: -1 } ?: -1
when { when {
it.data.isEmpty() -> view?.openLoginView() it.data.isEmpty() -> view?.openLoginView()
it.data.size == 1 -> { it.data.size == 1 -> onItemSelect(it.data.single().student)
selectedStudent = it.data.single().student
view?.showThemeDialog()
}
else -> view?.updateData(it.data, selectedStudentId) else -> view?.updateData(it.data, selectedStudentId)
} }
} }

View File

@ -7,8 +7,6 @@ interface LuckyNumberWidgetConfigureView : BaseView {
fun initView() fun initView()
fun showThemeDialog()
fun updateData(data: List<StudentWithSemesters>, selectedStudentId: Long) fun updateData(data: List<StudentWithSemesters>, selectedStudentId: Long)
fun updateLuckyNumberWidget(widgetId: Int) fun updateLuckyNumberWidget(widgetId: Int)

View File

@ -2,14 +2,12 @@ package io.github.wulkanowy.ui.modules.luckynumberwidget
import android.app.PendingIntent import android.app.PendingIntent
import android.appwidget.AppWidgetManager import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT
import android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH
import android.appwidget.AppWidgetProvider import android.appwidget.AppWidgetProvider
import android.content.Context import android.content.Context
import android.content.res.Configuration import android.content.res.Configuration
import android.os.Bundle import android.os.Bundle
import android.view.View.GONE import android.util.TypedValue.COMPLEX_UNIT_SP
import android.view.View.VISIBLE import android.view.View
import android.widget.RemoteViews import android.widget.RemoteViews
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R import io.github.wulkanowy.R
@ -17,7 +15,6 @@ import io.github.wulkanowy.data.Resource
import io.github.wulkanowy.data.dataOrNull import io.github.wulkanowy.data.dataOrNull
import io.github.wulkanowy.data.db.SharedPrefProvider import io.github.wulkanowy.data.db.SharedPrefProvider
import io.github.wulkanowy.data.db.entities.LuckyNumber import io.github.wulkanowy.data.db.entities.LuckyNumber
import io.github.wulkanowy.data.exceptions.NoCurrentStudentException
import io.github.wulkanowy.data.repositories.LuckyNumberRepository import io.github.wulkanowy.data.repositories.LuckyNumberRepository
import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.toFirstResult import io.github.wulkanowy.data.toFirstResult
@ -41,16 +38,12 @@ class LuckyNumberWidgetProvider : AppWidgetProvider() {
lateinit var sharedPref: SharedPrefProvider lateinit var sharedPref: SharedPrefProvider
companion object { companion object {
private const val LUCKY_NUMBER_WIDGET_MAX_SIZE = 196
const val LUCKY_NUMBER_PENDING_INTENT_ID = 200 private const val LUCKY_NUMBER_PENDING_INTENT_ID = 300
private const val LUCKY_NUMBER_HISTORY_PENDING_INTENT_ID = 301
fun getStudentWidgetKey(appWidgetId: Int) = "lucky_number_widget_student_$appWidgetId" fun getStudentWidgetKey(appWidgetId: Int) = "lucky_number_widget_student_$appWidgetId"
fun getThemeWidgetKey(appWidgetId: Int) = "lucky_number_widget_theme_$appWidgetId"
fun getHeightWidgetKey(appWidgetId: Int) = "lucky_number_widget_height_$appWidgetId"
fun getWidthWidgetKey(appWidgetId: Int) = "lucky_number_widget_width_$appWidgetId"
} }
override fun onUpdate( override fun onUpdate(
@ -59,107 +52,86 @@ class LuckyNumberWidgetProvider : AppWidgetProvider() {
appWidgetIds: IntArray? appWidgetIds: IntArray?
) { ) {
super.onUpdate(context, appWidgetManager, appWidgetIds) super.onUpdate(context, appWidgetManager, appWidgetIds)
appWidgetIds?.forEach { appWidgetId ->
val luckyNumber =
getLuckyNumber(sharedPref.getLong(getStudentWidgetKey(appWidgetId), 0), appWidgetId)
val appIntent = PendingIntent.getActivity(
context,
LUCKY_NUMBER_PENDING_INTENT_ID,
SplashActivity.getStartIntent(context, Destination.LuckyNumber),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE
)
if (luckyNumber is Resource.Error) { val appIntent = PendingIntent.getActivity(
Timber.e("Error loading lucky number for widget", luckyNumber.error) context,
} LUCKY_NUMBER_PENDING_INTENT_ID,
SplashActivity.getStartIntent(context, Destination.LuckyNumber),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE
)
val remoteView = val historyIntent = PendingIntent.getActivity(
RemoteViews(context.packageName, getCorrectLayoutId(appWidgetId, context)) context,
.apply { LUCKY_NUMBER_HISTORY_PENDING_INTENT_ID,
setTextViewText( SplashActivity.getStartIntent(context, Destination.LuckyNumberHistory),
R.id.luckyNumberWidgetNumber, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE
luckyNumber.dataOrNull?.luckyNumber?.toString() ?: "#" )
)
setOnClickPendingIntent(R.id.luckyNumberWidgetContainer, appIntent)
}
setStyles(remoteView, appWidgetId) appWidgetIds?.forEach { widgetId ->
appWidgetManager.updateAppWidget(appWidgetId, remoteView) val studentId = sharedPref.getLong(getStudentWidgetKey(widgetId), 0)
val luckyNumberResource = getLuckyNumber(studentId, widgetId)
val luckyNumber = luckyNumberResource.dataOrNull?.luckyNumber?.toString()
val remoteView = RemoteViews(context.packageName, R.layout.widget_luckynumber)
.apply {
setTextViewText(R.id.luckyNumberWidgetValue, luckyNumber ?: "-")
setOnClickPendingIntent(R.id.luckyNumberWidgetContainer, appIntent)
setOnClickPendingIntent(R.id.luckyNumberWidgetHistoryButton, historyIntent)
}
resizeWidget(context, appWidgetManager.getAppWidgetOptions(widgetId), remoteView)
appWidgetManager.updateAppWidget(widgetId, remoteView)
}
}
override fun onAppWidgetOptionsChanged(
context: Context?,
appWidgetManager: AppWidgetManager?,
appWidgetId: Int,
newOptions: Bundle?
) {
super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions)
if (context == null || newOptions == null || appWidgetManager == null) {
return
}
val remoteView = RemoteViews(context.packageName, R.layout.widget_luckynumber)
resizeWidget(context, newOptions, remoteView)
appWidgetManager.partiallyUpdateAppWidget(appWidgetId, remoteView)
}
private fun resizeWidget(context: Context, options: Bundle, remoteViews: RemoteViews) {
val (width, height) = options.getWidgetSize(context)
val size = minOf(width, height, LUCKY_NUMBER_WIDGET_MAX_SIZE).toFloat()
resizeWidgetContents(size, remoteViews)
Timber.v("LuckyNumberWidget resized: ${width}x${height} ($size)")
}
private fun resizeWidgetContents(size: Float, remoteViews: RemoteViews) {
var historyButtonVisibility = View.VISIBLE
var luckyNumberTextSize = 72f
if (size < 150) {
luckyNumberTextSize = 44f
historyButtonVisibility = View.GONE
}
if (size < 75) {
luckyNumberTextSize = 26f
}
remoteViews.apply {
setTextViewTextSize(R.id.luckyNumberWidgetValue, COMPLEX_UNIT_SP, luckyNumberTextSize)
setViewVisibility(R.id.luckyNumberWidgetHistoryButton, historyButtonVisibility)
} }
} }
override fun onDeleted(context: Context?, appWidgetIds: IntArray?) { override fun onDeleted(context: Context?, appWidgetIds: IntArray?) {
super.onDeleted(context, appWidgetIds) super.onDeleted(context, appWidgetIds)
appWidgetIds?.forEach { appWidgetId -> appWidgetIds?.forEach { appWidgetId ->
with(sharedPref) { sharedPref.delete(getStudentWidgetKey(appWidgetId))
delete(getHeightWidgetKey(appWidgetId))
delete(getStudentWidgetKey(appWidgetId))
delete(getThemeWidgetKey(appWidgetId))
delete(getWidthWidgetKey(appWidgetId))
}
} }
} }
override fun onAppWidgetOptionsChanged(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetId: Int,
newOptions: Bundle?
) {
super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions)
val remoteView = RemoteViews(context.packageName, getCorrectLayoutId(appWidgetId, context))
setStyles(remoteView, appWidgetId, newOptions)
appWidgetManager.updateAppWidget(appWidgetId, remoteView)
}
private fun setStyles(views: RemoteViews, appWidgetId: Int, options: Bundle? = null) {
val width = options?.getInt(OPTION_APPWIDGET_MIN_WIDTH) ?: sharedPref.getLong(
getWidthWidgetKey(appWidgetId), 74
).toInt()
val height = options?.getInt(OPTION_APPWIDGET_MAX_HEIGHT) ?: sharedPref.getLong(
getHeightWidgetKey(appWidgetId), 74
).toInt()
with(sharedPref) {
putLong(getWidthWidgetKey(appWidgetId), width.toLong())
putLong(getHeightWidgetKey(appWidgetId), height.toLong())
}
val rows = getCellsForSize(height)
val cols = getCellsForSize(width)
Timber.d("New lucky number widget measurement: %dx%d", width, height)
Timber.d("Widget size: $cols x $rows")
when {
1 == cols && 1 == rows -> views.setVisibility(imageTop = false, imageLeft = false)
1 == cols && 1 < rows -> views.setVisibility(imageTop = true, imageLeft = false)
1 < cols && 1 == rows -> views.setVisibility(imageTop = false, imageLeft = true)
1 == cols && 1 == rows -> views.setVisibility(imageTop = true, imageLeft = false)
2 == cols && 1 == rows -> views.setVisibility(imageTop = false, imageLeft = true)
else -> views.setVisibility(imageTop = false, imageLeft = false, title = true)
}
}
private fun RemoteViews.setVisibility(
imageTop: Boolean,
imageLeft: Boolean,
title: Boolean = false
) {
setViewVisibility(R.id.luckyNumberWidgetImageTop, if (imageTop) VISIBLE else GONE)
setViewVisibility(R.id.luckyNumberWidgetImageLeft, if (imageLeft) VISIBLE else GONE)
setViewVisibility(R.id.luckyNumberWidgetTitle, if (title) VISIBLE else GONE)
setViewVisibility(R.id.luckyNumberWidgetNumber, VISIBLE)
}
private fun getCellsForSize(size: Int): Int {
var n = 2
while (74 * n - 30 < size) ++n
return n - 1
}
private fun getLuckyNumber(studentId: Long, appWidgetId: Int) = runBlocking { private fun getLuckyNumber(studentId: Long, appWidgetId: Int) = runBlocking {
try { try {
val students = studentRepository.getSavedStudents() val students = studentRepository.getSavedStudents()
@ -181,22 +153,24 @@ class LuckyNumberWidgetProvider : AppWidgetProvider() {
Resource.Success<LuckyNumber?>(null) Resource.Success<LuckyNumber?>(null)
} }
} catch (e: Exception) { } catch (e: Exception) {
if (e.cause !is NoCurrentStudentException) { Timber.e(e, "An error has occurred in lucky number provider")
Timber.e(e, "An error has occurred in lucky number provider")
}
Resource.Error(e) Resource.Error(e)
} }
} }
private fun getCorrectLayoutId(appWidgetId: Int, context: Context): Int { private fun Bundle.getWidgetSize(context: Context): Pair<Int, Int> {
val savedTheme = sharedPref.getLong(getThemeWidgetKey(appWidgetId), 0) val minWidth = getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH)
val isSystemDarkMode = val maxWidth = getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH)
context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES val minHeight = getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT)
val maxHeight = getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT)
return if (savedTheme == 1L || (savedTheme == 2L && isSystemDarkMode)) { val orientation = context.resources.configuration.orientation
R.layout.widget_luckynumber_dark val isPortrait = orientation == Configuration.ORIENTATION_PORTRAIT
return if (isPortrait) {
minWidth to maxHeight
} else { } else {
R.layout.widget_luckynumber maxWidth to minHeight
} }
} }
} }

View File

@ -2,14 +2,14 @@ package io.github.wulkanowy.ui.modules.main
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build.VERSION_CODES.P import android.os.Build
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 android.view.ViewGroup.MarginLayoutParams
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback
import androidx.activity.addCallback import androidx.activity.addCallback
import androidx.core.view.ViewCompat import androidx.core.view.*
import androidx.core.view.isVisible
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.preference.Preference import androidx.preference.Preference
@ -27,6 +27,7 @@ import io.github.wulkanowy.databinding.DialogAdsConsentBinding
import io.github.wulkanowy.ui.base.BaseActivity import io.github.wulkanowy.ui.base.BaseActivity
import io.github.wulkanowy.ui.modules.Destination import io.github.wulkanowy.ui.modules.Destination
import io.github.wulkanowy.ui.modules.account.accountquick.AccountQuickDialog import io.github.wulkanowy.ui.modules.account.accountquick.AccountQuickDialog
import io.github.wulkanowy.ui.modules.settings.appearance.menuorder.AppMenuItem
import io.github.wulkanowy.utils.* import io.github.wulkanowy.utils.*
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
@ -89,8 +90,16 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(ActivityMainBinding.inflate(layoutInflater).apply { binding = this }.root) setContentView(ActivityMainBinding.inflate(layoutInflater).apply { binding = this }.root)
setSupportActionBar(binding.mainToolbar) setSupportActionBar(binding.mainToolbar)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
WindowCompat.setDecorFitsSystemWindows(window, false)
binding.mainAppBar.isLifted = true
}
initializeFragmentContainer()
this.savedInstanceState = savedInstanceState this.savedInstanceState = savedInstanceState
messageContainer = binding.mainMessageContainer messageContainer = binding.mainMessageContainer
messageAnchor = binding.mainMessageContainer
updateHelper.messageContainer = binding.mainFragmentContainer updateHelper.messageContainer = binding.mainFragmentContainer
onBackCallback = onBackPressedDispatcher.addCallback(this, enabled = false) { onBackCallback = onBackPressedDispatcher.addCallback(this, enabled = false) {
presenter.onBackPressed() presenter.onBackPressed()
@ -124,13 +133,20 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
return true return true
} }
override fun initView(startMenuIndex: Int, rootDestinations: List<Destination>) { override fun initView(
startMenuIndex: Int,
rootAppMenuItems: List<AppMenuItem>,
rootUpdatedDestinations: List<Destination>
) {
initializeToolbar() initializeToolbar()
initializeBottomNavigation(startMenuIndex) initializeBottomNavigation(startMenuIndex, rootAppMenuItems)
initializeNavController(startMenuIndex, rootDestinations) initializeNavController(startMenuIndex, rootUpdatedDestinations)
} }
private fun initializeNavController(startMenuIndex: Int, rootDestinations: List<Destination>) { private fun initializeNavController(
startMenuIndex: Int,
rootUpdatedDestinations: List<Destination>
) {
with(navController) { with(navController) {
setOnViewChangeListener { destinationView -> setOnViewChangeListener { destinationView ->
presenter.onViewChange(destinationView) presenter.onViewChange(destinationView)
@ -140,7 +156,7 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
) )
} }
fragmentHideStrategy = HIDE fragmentHideStrategy = HIDE
rootFragments = rootDestinations.map { it.destinationFragment } rootFragments = rootUpdatedDestinations.map { it.destinationFragment }
initialize(startMenuIndex, savedInstanceState) initialize(startMenuIndex, savedInstanceState)
} }
@ -156,17 +172,16 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
} }
} }
private fun initializeBottomNavigation(startMenuIndex: Int) { private fun initializeBottomNavigation(
startMenuIndex: Int,
rootAppMenuItems: List<AppMenuItem>
) {
with(binding.mainBottomNav) { with(binding.mainBottomNav) {
with(menu) { with(menu) {
add(Menu.NONE, 0, Menu.NONE, R.string.dashboard_title) rootAppMenuItems.forEachIndexed { index, item ->
.setIcon(R.drawable.ic_main_dashboard) add(Menu.NONE, index, Menu.NONE, item.title)
add(Menu.NONE, 1, Menu.NONE, R.string.grade_title) .setIcon(item.icon)
.setIcon(R.drawable.ic_main_grade) }
add(Menu.NONE, 2, Menu.NONE, R.string.attendance_title)
.setIcon(R.drawable.ic_main_attendance)
add(Menu.NONE, 3, Menu.NONE, R.string.timetable_title)
.setIcon(R.drawable.ic_main_timetable)
add(Menu.NONE, 4, Menu.NONE, R.string.more_title) add(Menu.NONE, 4, Menu.NONE, R.string.more_title)
.setIcon(R.drawable.ic_main_more) .setIcon(R.drawable.ic_main_more)
} }
@ -180,6 +195,17 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
} }
} }
private fun initializeFragmentContainer() {
ViewCompat.setOnApplyWindowInsetsListener(binding.mainFragmentContainer) { view, insets ->
val bottomInsets = insets.getInsets(WindowInsetsCompat.Type.navigationBars())
view.updateLayoutParams<MarginLayoutParams> {
bottomMargin = if (binding.mainBottomNav.isVisible) 0 else bottomInsets.bottom
}
WindowInsetsCompat.CONSUMED
}
}
override fun onPreferenceStartFragment( override fun onPreferenceStartFragment(
caller: PreferenceFragmentCompat, caller: PreferenceFragmentCompat,
pref: Preference pref: Preference
@ -224,20 +250,9 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
showDialogFragment(AccountQuickDialog.newInstance(studentWithSemesters)) showDialogFragment(AccountQuickDialog.newInstance(studentWithSemesters))
} }
override fun showActionBarElevation(show: Boolean) {
ViewCompat.setElevation(binding.mainToolbar, if (show) dpToPx(4f) else 0f)
}
override fun showBottomNavigation(show: Boolean) { override fun showBottomNavigation(show: Boolean) {
binding.mainBottomNav.isVisible = show binding.mainBottomNav.isVisible = show
binding.mainFragmentContainer.requestApplyInsets()
if (appInfo.systemVersion >= P) {
window.navigationBarColor = if (show) {
getThemeAttrColor(android.R.attr.navigationBarColor)
} else {
getThemeAttrColor(R.attr.colorSurface)
}
}
} }
override fun openMoreDestination(destination: Destination) { override fun openMoreDestination(destination: Destination) {

View File

@ -14,9 +14,6 @@ import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.ui.modules.Destination import io.github.wulkanowy.ui.modules.Destination
import io.github.wulkanowy.ui.modules.account.AccountView import io.github.wulkanowy.ui.modules.account.AccountView
import io.github.wulkanowy.ui.modules.account.accountdetails.AccountDetailsView import io.github.wulkanowy.ui.modules.account.accountdetails.AccountDetailsView
import io.github.wulkanowy.ui.modules.grade.GradeView
import io.github.wulkanowy.ui.modules.message.MessageView
import io.github.wulkanowy.ui.modules.schoolandteachers.SchoolAndTeachersView
import io.github.wulkanowy.ui.modules.studentinfo.StudentInfoView import io.github.wulkanowy.ui.modules.studentinfo.StudentInfoView
import io.github.wulkanowy.utils.AdsHelper import io.github.wulkanowy.utils.AdsHelper
import io.github.wulkanowy.utils.AnalyticsHelper import io.github.wulkanowy.utils.AnalyticsHelper
@ -42,17 +39,16 @@ class MainPresenter @Inject constructor(
private var studentsWitSemesters: List<StudentWithSemesters>? = null private var studentsWitSemesters: List<StudentWithSemesters>? = null
private val rootDestinationTypeList = listOf( private val rootAppMenuItems = preferencesRepository.appMenuItemOrder
Destination.Type.DASHBOARD, .sortedBy { it.order }
Destination.Type.GRADE, .take(4)
Destination.Type.ATTENDANCE,
Destination.Type.TIMETABLE, private val rootDestinationTypeList = rootAppMenuItems.map { it.destinationType }
Destination.Type.MORE .plus(Destination.Type.MORE)
)
private val Destination?.startMenuIndex private val Destination?.startMenuIndex
get() = when { get() = when {
this == null -> preferencesRepository.startMenuIndex this == null -> 0
destinationType in rootDestinationTypeList -> { destinationType in rootDestinationTypeList -> {
rootDestinationTypeList.indexOf(destinationType) rootDestinationTypeList.indexOf(destinationType)
} }
@ -69,7 +65,7 @@ class MainPresenter @Inject constructor(
if (it == initDestination?.destinationType) initDestination else it.defaultDestination if (it == initDestination?.destinationType) initDestination else it.defaultDestination
} }
view.initView(startMenuIndex, destinations) view.initView(startMenuIndex, rootAppMenuItems, destinations)
if (initDestination != null && startMenuIndex == 4) { if (initDestination != null && startMenuIndex == 4) {
view.openMoreDestination(initDestination) view.openMoreDestination(initDestination)
} }
@ -101,7 +97,6 @@ class MainPresenter @Inject constructor(
fun onViewChange(destinationView: BaseView) { fun onViewChange(destinationView: BaseView) {
view?.apply { view?.apply {
showBottomNavigation(shouldShowBottomNavigation(destinationView)) showBottomNavigation(shouldShowBottomNavigation(destinationView))
showActionBarElevation(shouldShowActionBarElevation(destinationView))
currentViewTitle?.let { setViewTitle(it) } currentViewTitle?.let { setViewTitle(it) }
currentViewSubtitle?.let { setViewSubTitle(it.ifBlank { null }) } currentViewSubtitle?.let { setViewSubTitle(it.ifBlank { null }) }
currentStackSize?.let { currentStackSize?.let {
@ -111,13 +106,6 @@ class MainPresenter @Inject constructor(
} }
} }
private fun shouldShowActionBarElevation(destination: BaseView) = when (destination) {
is GradeView,
is MessageView,
is SchoolAndTeachersView -> false
else -> true
}
private fun shouldShowBottomNavigation(destination: BaseView) = when (destination) { private fun shouldShowBottomNavigation(destination: BaseView) = when (destination) {
is AccountView, is AccountView,
is StudentInfoView, is StudentInfoView,

View File

@ -4,6 +4,7 @@ import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentWithSemesters 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.Destination import io.github.wulkanowy.ui.modules.Destination
import io.github.wulkanowy.ui.modules.settings.appearance.menuorder.AppMenuItem
interface MainView : BaseView { interface MainView : BaseView {
@ -15,7 +16,11 @@ interface MainView : BaseView {
val currentStackSize: Int? val currentStackSize: Int?
fun initView(startMenuIndex: Int, rootDestinations: List<Destination>) fun initView(
startMenuIndex: Int,
rootAppMenuItems: List<AppMenuItem>,
rootUpdatedDestinations: List<Destination>
)
fun switchMenuView(position: Int) fun switchMenuView(position: Int)
@ -23,8 +28,6 @@ interface MainView : BaseView {
fun showAccountPicker(studentWithSemesters: List<StudentWithSemesters>) fun showAccountPicker(studentWithSemesters: List<StudentWithSemesters>)
fun showActionBarElevation(show: Boolean)
fun showBottomNavigation(show: Boolean) fun showBottomNavigation(show: Boolean)
fun notifyMenuViewReselected() fun notifyMenuViewReselected()

View File

@ -15,6 +15,7 @@ import io.github.wulkanowy.data.enums.MessageFolder.*
import io.github.wulkanowy.databinding.FragmentMessageBinding import io.github.wulkanowy.databinding.FragmentMessageBinding
import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.base.BaseFragmentPagerAdapter import io.github.wulkanowy.ui.base.BaseFragmentPagerAdapter
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.ui.modules.message.tab.MessageTabFragment import io.github.wulkanowy.ui.modules.message.tab.MessageTabFragment
@ -24,7 +25,7 @@ import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class MessageFragment : BaseFragment<FragmentMessageBinding>(R.layout.fragment_message), class MessageFragment : BaseFragment<FragmentMessageBinding>(R.layout.fragment_message),
MessageView, MainView.TitledView { MessageView, MainView.TitledView, MainView.MainChildView {
@Inject @Inject
lateinit var presenter: MessagePresenter lateinit var presenter: MessagePresenter
@ -123,8 +124,12 @@ class MessageFragment : BaseFragment<FragmentMessageBinding>(R.layout.fragment_m
presenter.onChildViewShowNewMessage(show) presenter.onChildViewShowNewMessage(show)
} }
fun onFragmentChanged() { override fun onFragmentReselected() {
presenter.onFragmentChanged() if (::presenter.isInitialized) presenter.onFragmentReselected()
}
override fun onFragmentChanged() {
if (::presenter.isInitialized) presenter.onFragmentChanged()
} }
override fun notifyChildLoadData(index: Int, forceRefresh: Boolean) { override fun notifyChildLoadData(index: Int, forceRefresh: Boolean) {
@ -139,10 +144,19 @@ class MessageFragment : BaseFragment<FragmentMessageBinding>(R.layout.fragment_m
} }
} }
override fun notifyChildParentReselected(index: Int) {
(pagerAdapter.getFragmentInstance(index) as? MessageTabFragment)
?.onParentReselected()
}
override fun openSendMessage() { override fun openSendMessage() {
context?.let { it.startActivity(SendMessageActivity.getStartIntent(it)) } context?.let { it.startActivity(SendMessageActivity.getStartIntent(it)) }
} }
override fun popView() {
(activity as? MainActivity)?.popView()
}
override fun onDestroyView() { override fun onDestroyView() {
presenter.onDetachView() presenter.onDetachView()
super.onDestroyView() super.onDestroyView()

View File

@ -39,6 +39,14 @@ class MessagePresenter @Inject constructor(
view?.notifyChildrenFinishActionMode() view?.notifyChildrenFinishActionMode()
} }
fun onFragmentReselected() {
Timber.i("Message view is reselected")
view?.run {
popView()
notifyChildParentReselected(currentPageIndex)
}
}
fun onChildViewLoaded() { fun onChildViewLoaded() {
view?.apply { view?.apply {
showContent(true) showContent(true)

View File

@ -20,5 +20,9 @@ interface MessageView : BaseView {
fun notifyChildrenFinishActionMode() fun notifyChildrenFinishActionMode()
fun notifyChildParentReselected(index: Int)
fun openSendMessage() fun openSendMessage()
fun popView()
} }

View File

@ -1,11 +1,11 @@
package io.github.wulkanowy.ui.modules.message.mailboxchooser package io.github.wulkanowy.ui.modules.message.mailboxchooser
import android.app.Dialog
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.fragment.app.setFragmentResult import androidx.fragment.app.setFragmentResult
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint 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
@ -37,19 +37,19 @@ class MailboxChooserDialog : BaseDialogFragment<DialogMailboxChooserBinding>(),
} }
} }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
setStyle(STYLE_NO_TITLE, 0) return MaterialAlertDialogBuilder(requireContext(), theme)
.setView(
DialogMailboxChooserBinding.inflate(layoutInflater).apply { binding = this }.root
)
.create()
} }
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
) = DialogMailboxChooserBinding.inflate(inflater).apply { binding = this }.root
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
presenter.onAttachView( presenter.onAttachView(
view = this, view = this,
requireMailbox = requireArguments().getBoolean(REQUIRED_KEY, false), requireMailbox = requireArguments().getBoolean(REQUIRED_KEY, false),

View File

@ -1,10 +1,10 @@
package io.github.wulkanowy.ui.modules.message.send package io.github.wulkanowy.ui.modules.message.send
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.AlertDialog
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Rect import android.graphics.Rect
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.text.Spanned import android.text.Spanned
import android.view.Menu import android.view.Menu
@ -12,11 +12,14 @@ import android.view.MenuItem
import android.view.TouchDelegate import android.view.TouchDelegate
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.widget.Toast import android.widget.Toast
import android.widget.Toast.LENGTH_LONG import android.widget.Toast.LENGTH_LONG
import androidx.core.text.parseAsHtml import androidx.core.text.parseAsHtml
import androidx.core.text.toHtml import androidx.core.text.toHtml
import androidx.core.view.*
import androidx.core.widget.doOnTextChanged import androidx.core.widget.doOnTextChanged
import com.google.android.material.dialog.MaterialAlertDialogBuilder
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.Mailbox import io.github.wulkanowy.data.db.entities.Mailbox
@ -24,8 +27,8 @@ import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.databinding.ActivitySendMessageBinding import io.github.wulkanowy.databinding.ActivitySendMessageBinding
import io.github.wulkanowy.ui.base.BaseActivity import io.github.wulkanowy.ui.base.BaseActivity
import io.github.wulkanowy.ui.modules.message.mailboxchooser.MailboxChooserDialog import io.github.wulkanowy.ui.modules.message.mailboxchooser.MailboxChooserDialog
import io.github.wulkanowy.ui.modules.message.mailboxchooser.MailboxChooserDialog.Companion.MAILBOX_KEY
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.ui.modules.message.mailboxchooser.MailboxChooserDialog.Companion.MAILBOX_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.nullableSerializable
@ -99,6 +102,13 @@ class SendMessageActivity : BaseActivity<SendMessagePresenter, ActivitySendMessa
) )
setSupportActionBar(binding.sendMessageToolbar) setSupportActionBar(binding.sendMessageToolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
WindowCompat.setDecorFitsSystemWindows(window, false)
binding.sendAppBar.isLifted = true
}
initializeMessageContainer()
messageContainer = binding.sendMessageContainer messageContainer = binding.sendMessageContainer
formRecipientsData = binding.sendMessageTo.addedChipItems as List<RecipientChipItem> formRecipientsData = binding.sendMessageTo.addedChipItems as List<RecipientChipItem>
@ -130,6 +140,17 @@ class SendMessageActivity : BaseActivity<SendMessagePresenter, ActivitySendMessa
} }
} }
private fun initializeMessageContainer() {
ViewCompat.setOnApplyWindowInsetsListener(binding.sendMessageScroll) { view, insets ->
val bottomInsets = insets.getInsets(WindowInsetsCompat.Type.navigationBars())
view.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = bottomInsets.bottom
}
WindowInsetsCompat.CONSUMED
}
}
private fun onMessageSubjectChange(text: CharSequence?) { private fun onMessageSubjectChange(text: CharSequence?) {
formSubjectValue = text.toString() formSubjectValue = text.toString()
presenter.onMessageContentChange() presenter.onMessageContentChange()
@ -252,7 +273,7 @@ class SendMessageActivity : BaseActivity<SendMessagePresenter, ActivitySendMessa
} }
override fun showMessageBackupDialog() { override fun showMessageBackupDialog() {
AlertDialog.Builder(this) MaterialAlertDialogBuilder(this)
.setTitle(R.string.message_title) .setTitle(R.string.message_title)
.setMessage(presenter.getMessageBackupContent(presenter.getRecipientsNames())) .setMessage(presenter.getMessageBackupContent(presenter.getRecipientsNames()))
.setPositiveButton(R.string.all_yes) { _, _ -> presenter.restoreMessageParts() } .setPositiveButton(R.string.all_yes) { _, _ -> presenter.restoreMessageParts() }

View File

@ -1,5 +1,6 @@
package io.github.wulkanowy.ui.modules.message.tab package io.github.wulkanowy.ui.modules.message.tab
import android.annotation.SuppressLint
import android.content.res.ColorStateList import android.content.res.ColorStateList
import android.graphics.Typeface import android.graphics.Typeface
import android.view.LayoutInflater import android.view.LayoutInflater
@ -68,21 +69,23 @@ class MessageTabAdapter @Inject constructor() :
} }
} }
@SuppressLint("PrivateResource")
private fun bindHeaderViewHolder(holder: HeaderViewHolder, position: Int) { private fun bindHeaderViewHolder(holder: HeaderViewHolder, position: Int) {
val item = items[position] as MessageTabDataItem.FilterHeader val item = items[position] as MessageTabDataItem.FilterHeader
val context = holder.binding.root.context
with(holder.binding) { with(holder.binding) {
chipMailbox.text = item.selectedMailbox chipMailbox.text =
?: root.context.getString(R.string.message_chip_all_mailboxes) item.selectedMailbox ?: context.getString(R.string.message_chip_all_mailboxes)
chipMailbox.chipBackgroundColor = ColorStateList.valueOf( chipMailbox.chipBackgroundColor = ColorStateList.valueOf(
if (item.selectedMailbox == null) { if (item.selectedMailbox == null) {
root.context.getCompatColor(R.color.mtrl_choice_chip_background_color) context.getCompatColor(R.color.m3_elevated_chip_background_color)
} else root.context.getThemeAttrColor(android.R.attr.colorPrimary, 64) } else context.getThemeAttrColor(R.attr.colorPrimary, 64)
) )
chipMailbox.setTextColor( chipMailbox.setTextColor(
if (item.selectedMailbox == null) { if (item.selectedMailbox == null) {
root.context.getThemeAttrColor(android.R.attr.textColorPrimary) context.getThemeAttrColor(R.attr.colorOnSurfaceVariant)
} else root.context.getThemeAttrColor(android.R.attr.colorPrimary) } else context.getThemeAttrColor(R.attr.colorPrimary)
) )
chipMailbox.setOnClickListener { onMailboxClickListener() } chipMailbox.setOnClickListener { onMailboxClickListener() }

View File

@ -235,6 +235,10 @@ class MessageTabFragment : BaseFragment<FragmentMessageTabBinding>(R.layout.frag
presenter.onParentFinishActionMode() presenter.onParentFinishActionMode()
} }
fun onParentReselected() {
presenter.onParentReselected()
}
private fun onChipChecked(chip: CompoundButton, isChecked: Boolean) { private fun onChipChecked(chip: CompoundButton, isChecked: Boolean) {
when (chip.id) { when (chip.id) {
R.id.chip_unread -> presenter.onUnreadFilterSelected(isChecked) R.id.chip_unread -> presenter.onUnreadFilterSelected(isChecked)

View File

@ -83,6 +83,14 @@ class MessageTabPresenter @Inject constructor(
view?.showActionMode(false) view?.showActionMode(false)
} }
fun onParentReselected() {
view?.run {
if (!isViewEmpty) {
resetListPosition()
}
}
}
fun onDestroyActionMode() { fun onDestroyActionMode() {
isActionMode = false isActionMode = false
messagesToDelete.clear() messagesToDelete.clear()

View File

@ -22,7 +22,7 @@ import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class MobileDeviceFragment : class MobileDeviceFragment :
BaseFragment<FragmentMobileDeviceBinding>(R.layout.fragment_mobile_device), MobileDeviceView, BaseFragment<FragmentMobileDeviceBinding>(R.layout.fragment_mobile_device), MobileDeviceView,
MainView.TitledView { MainView.TitledView, MainView.MainChildView {
@Inject @Inject
lateinit var presenter: MobileDevicePresenter lateinit var presenter: MobileDevicePresenter
@ -135,6 +135,14 @@ class MobileDeviceFragment :
(activity as? MainActivity)?.showDialogFragment(MobileDeviceTokenDialog.newInstance()) (activity as? MainActivity)?.showDialogFragment(MobileDeviceTokenDialog.newInstance())
} }
override fun onFragmentReselected() {
if (::presenter.isInitialized) presenter.onFragmentReselected()
}
override fun resetView() {
binding.mobileDevicesRecycler.smoothScrollToPosition(0)
}
override fun onDestroyView() { override fun onDestroyView() {
presenter.onDetachView() presenter.onDetachView()
super.onDestroyView() super.onDestroyView()

View File

@ -129,4 +129,10 @@ class MobileDevicePresenter @Inject constructor(
.onResourceError(errorHandler::dispatch) .onResourceError(errorHandler::dispatch)
.launch("unregister") .launch("unregister")
} }
fun onFragmentReselected() {
if (view?.isViewEmpty == false) {
view?.resetView()
}
}
} }

View File

@ -32,4 +32,6 @@ interface MobileDeviceView : BaseView {
fun setErrorDetails(message: String) fun setErrorDetails(message: String)
fun showTokenDialog() fun showTokenDialog()
fun resetView()
} }

View File

@ -1,17 +1,17 @@
package io.github.wulkanowy.ui.modules.mobiledevice.token package io.github.wulkanowy.ui.modules.mobiledevice.token
import android.app.Dialog
import android.content.ClipData import android.content.ClipData
import android.content.ClipboardManager import android.content.ClipboardManager
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.os.Bundle import android.os.Bundle
import android.util.Base64 import android.util.Base64
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.View.VISIBLE import android.view.View.VISIBLE
import android.view.ViewGroup
import android.widget.Toast import android.widget.Toast
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import com.google.android.material.dialog.MaterialAlertDialogBuilder
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.pojos.MobileDeviceToken import io.github.wulkanowy.data.pojos.MobileDeviceToken
@ -31,17 +31,14 @@ class MobileDeviceTokenDialog : BaseDialogFragment<DialogMobileDeviceBinding>(),
fun newInstance() = MobileDeviceTokenDialog() fun newInstance() = MobileDeviceTokenDialog()
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
super.onCreate(savedInstanceState) return MaterialAlertDialogBuilder(requireContext(), theme)
setStyle(STYLE_NO_TITLE, 0) .setView(
DialogMobileDeviceBinding.inflate(layoutInflater).apply { binding = this }.root
)
.create()
} }
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
) = DialogMobileDeviceBinding.inflate(inflater).apply { binding = this }.root
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) presenter.onAttachView(this)

View File

@ -1,6 +1,5 @@
package io.github.wulkanowy.ui.modules.more package io.github.wulkanowy.ui.modules.more
import android.graphics.drawable.Drawable
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@ -9,9 +8,9 @@ import javax.inject.Inject
class MoreAdapter @Inject constructor() : RecyclerView.Adapter<MoreAdapter.ItemViewHolder>() { class MoreAdapter @Inject constructor() : RecyclerView.Adapter<MoreAdapter.ItemViewHolder>() {
var items = emptyList<Pair<String, Drawable?>>() var items = emptyList<MoreItem>()
var onClickListener: (name: String) -> Unit = {} var onClickListener: (moreItem: MoreItem) -> Unit = {}
override fun getItemCount() = items.size override fun getItemCount() = items.size
@ -20,13 +19,14 @@ class MoreAdapter @Inject constructor() : RecyclerView.Adapter<MoreAdapter.ItemV
) )
override fun onBindViewHolder(holder: ItemViewHolder, position: Int) { override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
val (title, drawable) = items[position] val item = items[position]
val context = holder.binding.root.context
with(holder.binding) { with(holder.binding) {
moreItemTitle.text = title moreItemTitle.text = context.getString(item.title)
moreItemImage.setImageDrawable(drawable) moreItemImage.setImageResource(item.icon)
root.setOnClickListener { onClickListener(title) } root.setOnClickListener { onClickListener(item) }
} }
} }

View File

@ -1,6 +1,5 @@
package io.github.wulkanowy.ui.modules.more package io.github.wulkanowy.ui.modules.more
import android.graphics.drawable.Drawable
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
@ -8,19 +7,10 @@ import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.databinding.FragmentMoreBinding import io.github.wulkanowy.databinding.FragmentMoreBinding
import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.conference.ConferenceFragment import io.github.wulkanowy.ui.modules.Destination
import io.github.wulkanowy.ui.modules.exam.ExamFragment
import io.github.wulkanowy.ui.modules.homework.HomeworkFragment
import io.github.wulkanowy.ui.modules.luckynumber.LuckyNumberFragment
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.MessageFragment import io.github.wulkanowy.ui.modules.message.MessageFragment
import io.github.wulkanowy.ui.modules.mobiledevice.MobileDeviceFragment
import io.github.wulkanowy.ui.modules.note.NoteFragment
import io.github.wulkanowy.ui.modules.schoolandteachers.SchoolAndTeachersFragment
import io.github.wulkanowy.ui.modules.schoolannouncement.SchoolAnnouncementFragment
import io.github.wulkanowy.ui.modules.settings.SettingsFragment
import io.github.wulkanowy.utils.getCompatDrawable
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
@ -40,36 +30,6 @@ class MoreFragment : BaseFragment<FragmentMoreBinding>(R.layout.fragment_more),
override val titleStringId: Int override val titleStringId: Int
get() = R.string.more_title get() = R.string.more_title
override val messagesRes: Pair<String, Drawable?>?
get() = context?.run { getString(R.string.message_title) to getCompatDrawable(R.drawable.ic_more_messages) }
override val homeworkRes: Pair<String, Drawable?>?
get() = context?.run { getString(R.string.homework_title) to getCompatDrawable(R.drawable.ic_more_homework) }
override val noteRes: Pair<String, Drawable?>?
get() = context?.run { getString(R.string.note_title) to getCompatDrawable(R.drawable.ic_more_note) }
override val conferencesRes: Pair<String, Drawable?>?
get() = context?.run { getString(R.string.conferences_title) to getCompatDrawable(R.drawable.ic_more_conferences) }
override val schoolAnnouncementRes: Pair<String, Drawable?>?
get() = context?.run { getString(R.string.school_announcement_title) to getCompatDrawable(R.drawable.ic_all_about) }
override val schoolAndTeachersRes: Pair<String, Drawable?>?
get() = context?.run { getString(R.string.schoolandteachers_title) to getCompatDrawable((R.drawable.ic_more_schoolandteachers)) }
override val mobileDevicesRes: Pair<String, Drawable?>?
get() = context?.run { getString(R.string.mobile_devices_title) to getCompatDrawable(R.drawable.ic_more_mobile_devices) }
override val settingsRes: Pair<String, Drawable?>?
get() = context?.run { getString(R.string.settings_title) to getCompatDrawable(R.drawable.ic_more_settings) }
override val examRes: Pair<String, Drawable?>?
get() = context?.run { getString(R.string.exam_title) to getCompatDrawable(R.drawable.ic_main_exam) }
override val luckyNumberRes: Pair<String, Drawable?>?
get() = context?.run { getString(R.string.lucky_number_title) to getCompatDrawable(R.drawable.ic_more_lucky_number) }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
binding = FragmentMoreBinding.bind(view) binding = FragmentMoreBinding.bind(view)
@ -94,57 +54,21 @@ class MoreFragment : BaseFragment<FragmentMoreBinding>(R.layout.fragment_more),
?.onFragmentChanged() ?.onFragmentChanged()
} }
override fun updateData(data: List<Pair<String, Drawable?>>) { override fun updateData(data: List<MoreItem>) {
with(moreAdapter) { with(moreAdapter) {
items = data items = data
notifyDataSetChanged() notifyDataSetChanged()
} }
} }
override fun openMessagesView() {
(activity as? MainActivity)?.pushView(MessageFragment.newInstance())
}
override fun openHomeworkView() {
(activity as? MainActivity)?.pushView(HomeworkFragment.newInstance())
}
override fun openNoteView() {
(activity as? MainActivity)?.pushView(NoteFragment.newInstance())
}
override fun openSchoolAnnouncementView() {
(activity as? MainActivity)?.pushView(SchoolAnnouncementFragment.newInstance())
}
override fun openConferencesView() {
(activity as? MainActivity)?.pushView(ConferenceFragment.newInstance())
}
override fun openSchoolAndTeachersView() {
(activity as? MainActivity)?.pushView(SchoolAndTeachersFragment.newInstance())
}
override fun openMobileDevicesView() {
(activity as? MainActivity)?.pushView(MobileDeviceFragment.newInstance())
}
override fun openSettingsView() {
(activity as? MainActivity)?.pushView(SettingsFragment.newInstance())
}
override fun openExamView() {
(activity as? MainActivity)?.pushView(ExamFragment.newInstance())
}
override fun openLuckyNumberView() {
(activity as? MainActivity)?.pushView(LuckyNumberFragment.newInstance())
}
override fun popView(depth: Int) { override fun popView(depth: Int) {
(activity as? MainActivity)?.popView(depth) (activity as? MainActivity)?.popView(depth)
} }
override fun openView(destination: Destination) {
(activity as? MainActivity)?.pushView(destination.destinationFragment)
}
override fun onDestroyView() { override fun onDestroyView() {
presenter.onDetachView() presenter.onDetachView()
super.onDestroyView() super.onDestroyView()

View File

@ -0,0 +1,12 @@
package io.github.wulkanowy.ui.modules.more
import io.github.wulkanowy.ui.modules.Destination
data class MoreItem(
val icon: Int,
val title: Int,
val destination: Destination
)

View File

@ -1,16 +1,24 @@
package io.github.wulkanowy.ui.modules.more package io.github.wulkanowy.ui.modules.more
import io.github.wulkanowy.R
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.ui.modules.Destination
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
class MorePresenter @Inject constructor( class MorePresenter @Inject constructor(
errorHandler: ErrorHandler, errorHandler: ErrorHandler,
studentRepository: StudentRepository studentRepository: StudentRepository,
preferencesRepository: PreferencesRepository
) : BasePresenter<MoreView>(errorHandler, studentRepository) { ) : BasePresenter<MoreView>(errorHandler, studentRepository) {
private val moreAppMenuItem = preferencesRepository.appMenuItemOrder
.sortedBy { it.order }
.drop(4)
override fun onAttachView(view: MoreView) { override fun onAttachView(view: MoreView) {
super.onAttachView(view) super.onAttachView(view)
view.initView() view.initView()
@ -18,22 +26,10 @@ class MorePresenter @Inject constructor(
loadData() loadData()
} }
fun onItemSelected(title: String) { fun onItemSelected(moreItem: MoreItem) {
Timber.i("Select more item \"${title}\"") Timber.i("Select more item \"${moreItem.destination.destinationType}\"")
view?.run {
when (title) { view?.openView(moreItem.destination)
messagesRes?.first -> openMessagesView()
examRes?.first -> openExamView()
homeworkRes?.first -> openHomeworkView()
noteRes?.first -> openNoteView()
conferencesRes?.first -> openConferencesView()
schoolAnnouncementRes?.first -> openSchoolAnnouncementView()
schoolAndTeachersRes?.first -> openSchoolAndTeachersView()
mobileDevicesRes?.first -> openMobileDevicesView()
settingsRes?.first -> openSettingsView()
luckyNumberRes?.first -> openLuckyNumberView()
}
}
} }
fun onViewReselected() { fun onViewReselected() {
@ -43,19 +39,21 @@ class MorePresenter @Inject constructor(
private fun loadData() { private fun loadData() {
Timber.i("Load items for more view") Timber.i("Load items for more view")
view?.run { val moreItems = moreAppMenuItem.map {
updateData(listOfNotNull( MoreItem(
messagesRes, icon = it.icon,
examRes, title = it.title,
homeworkRes, destination = it.destinationType.defaultDestination
noteRes, )
luckyNumberRes,
conferencesRes,
schoolAnnouncementRes,
schoolAndTeachersRes,
mobileDevicesRes,
settingsRes
))
} }
.plus(
MoreItem(
icon = R.drawable.ic_more_settings,
title = R.string.settings_title,
destination = Destination.Settings
)
)
view?.updateData(moreItems)
} }
} }

View File

@ -1,53 +1,15 @@
package io.github.wulkanowy.ui.modules.more package io.github.wulkanowy.ui.modules.more
import android.graphics.drawable.Drawable
import io.github.wulkanowy.ui.base.BaseView import io.github.wulkanowy.ui.base.BaseView
import io.github.wulkanowy.ui.modules.Destination
interface MoreView : BaseView { interface MoreView : BaseView {
val messagesRes: Pair<String, Drawable?>?
val homeworkRes: Pair<String, Drawable?>?
val noteRes: Pair<String, Drawable?>?
val conferencesRes: Pair<String, Drawable?>?
val schoolAnnouncementRes: Pair<String, Drawable?>?
val schoolAndTeachersRes: Pair<String, Drawable?>?
val mobileDevicesRes: Pair<String, Drawable?>?
val settingsRes: Pair<String, Drawable?>?
val examRes: Pair<String, Drawable?>?
val luckyNumberRes: Pair<String, Drawable?>?
fun initView() fun initView()
fun updateData(data: List<Pair<String, Drawable?>>) fun updateData(data: List<MoreItem>)
fun openSettingsView()
fun popView(depth: Int) fun popView(depth: Int)
fun openMessagesView() fun openView(destination: Destination)
fun openHomeworkView()
fun openNoteView()
fun openSchoolAnnouncementView()
fun openConferencesView()
fun openSchoolAndTeachersView()
fun openMobileDevicesView()
fun openExamView()
fun openLuckyNumberView()
} }

View File

@ -1,25 +1,24 @@
package io.github.wulkanowy.ui.modules.note package io.github.wulkanowy.ui.modules.note
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Dialog
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.fragment.app.DialogFragment import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
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
import io.github.wulkanowy.databinding.DialogNoteBinding 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.ui.base.BaseDialogFragment
import io.github.wulkanowy.utils.getThemeAttrColor import io.github.wulkanowy.utils.getThemeAttrColor
import io.github.wulkanowy.utils.lifecycleAwareVariable
import io.github.wulkanowy.utils.serializable import io.github.wulkanowy.utils.serializable
import io.github.wulkanowy.utils.toFormattedString import io.github.wulkanowy.utils.toFormattedString
class NoteDialog : DialogFragment() { @AndroidEntryPoint
class NoteDialog : BaseDialogFragment<DialogNoteBinding>() {
private var binding: DialogNoteBinding by lifecycleAwareVariable()
private lateinit var note: Note private lateinit var note: Note
@ -34,15 +33,14 @@ class NoteDialog : DialogFragment() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setStyle(STYLE_NO_TITLE, 0)
note = requireArguments().serializable(ARGUMENT_KEY) note = requireArguments().serializable(ARGUMENT_KEY)
} }
override fun onCreateView( override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
inflater: LayoutInflater, return MaterialAlertDialogBuilder(requireContext(), theme)
container: ViewGroup?, .setView(DialogNoteBinding.inflate(layoutInflater).apply { binding = this }.root)
savedInstanceState: Bundle? .create()
) = DialogNoteBinding.inflate(inflater).apply { binding = this }.root }
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {

View File

@ -18,7 +18,7 @@ import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class NoteFragment : BaseFragment<FragmentNoteBinding>(R.layout.fragment_note), NoteView, class NoteFragment : BaseFragment<FragmentNoteBinding>(R.layout.fragment_note), NoteView,
MainView.TitledView { MainView.TitledView, MainView.MainChildView {
@Inject @Inject
lateinit var presenter: NotePresenter lateinit var presenter: NotePresenter
@ -112,6 +112,14 @@ class NoteFragment : BaseFragment<FragmentNoteBinding>(R.layout.fragment_note),
binding.noteSwipe.isRefreshing = show binding.noteSwipe.isRefreshing = show
} }
override fun onFragmentReselected() {
if (::presenter.isInitialized) presenter.onFragmentReselected()
}
override fun resetView() {
binding.noteRecycler.smoothScrollToPosition(0)
}
override fun onDestroyView() { override fun onDestroyView() {
presenter.onDetachView() presenter.onDetachView()
super.onDestroyView() super.onDestroyView()

View File

@ -121,4 +121,10 @@ class NotePresenter @Inject constructor(
} }
.launch("update_note") .launch("update_note")
} }
fun onFragmentReselected() {
if (view?.isViewEmpty == false) {
view?.resetView()
}
}
} }

View File

@ -30,4 +30,6 @@ interface NoteView : BaseView {
fun showRefresh(show: Boolean) fun showRefresh(show: Boolean)
fun showNoteDialog(note: Note) fun showNoteDialog(note: Note)
fun resetView()
} }

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