1
0
Fork 1

Compare commits

..

No commits in common. "2.2.4" and "1.9.1" have entirely different histories.
2.2.4 ... 1.9.1

395 changed files with 4016 additions and 15389 deletions

View file

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

View file

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

View file

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

1
.gitignore vendored
View file

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

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

View file

@ -1,13 +0,0 @@
package io.github.wulkanowy.utils
import android.view.View
import javax.inject.Inject
class InAppUpdateHelper @Inject constructor() {
lateinit var messageContainer: View
fun checkAndInstallUpdates() {}
fun onResume() {}
}

View file

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

View file

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

View file

@ -0,0 +1,17 @@
package io.github.wulkanowy.utils
import android.app.Activity
import android.view.View
import javax.inject.Inject
@Suppress("UNUSED_PARAMETER")
class UpdateHelper @Inject constructor() {
lateinit var messageContainer: View
fun checkAndInstallUpdates(activity: Activity) {}
fun onActivityResult(requestCode: Int, resultCode: Int) {}
fun onResume(activity: Activity) {}
}

View file

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

View file

@ -1,13 +0,0 @@
package io.github.wulkanowy.utils
import android.view.View
import javax.inject.Inject
class InAppUpdateHelper @Inject constructor() {
lateinit var messageContainer: View
fun checkAndInstallUpdates() {}
fun onResume() {}
}

View file

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

View file

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

View file

@ -0,0 +1,17 @@
package io.github.wulkanowy.utils
import android.app.Activity
import android.view.View
import javax.inject.Inject
@Suppress("UNUSED_PARAMETER")
class UpdateHelper @Inject constructor() {
lateinit var messageContainer: View
fun checkAndInstallUpdates(activity: Activity) {}
fun onActivityResult(requestCode: Int, resultCode: Int) {}
fun onResume(activity: Activity) {}
}

View file

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

View file

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

View file

@ -6,7 +6,7 @@ import androidx.hilt.work.HiltWorkerFactory
import androidx.work.Configuration
import com.yariksoffice.lingver.Lingver
import dagger.hilt.android.HiltAndroidApp
import fr.bipi.treessence.file.FileLoggerTree
import fr.bipi.tressence.file.FileLoggerTree
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.ui.base.ThemeManager
import io.github.wulkanowy.utils.*
@ -34,15 +34,11 @@ class WulkanowyApp : Application(), Configuration.Provider {
@Inject
lateinit var adsHelper: AdsHelper
@Inject
lateinit var remoteConfigHelper: RemoteConfigHelper
override fun onCreate() {
super.onCreate()
initializeAppLanguage()
themeManager.applyDefaultTheme()
adsHelper.initialize()
remoteConfigHelper.initialize()
initLogging()
}

View file

@ -14,13 +14,12 @@ import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import io.github.wulkanowy.data.api.AdminMessageService
import io.github.wulkanowy.data.api.SchoolsService
import io.github.wulkanowy.data.db.AppDatabase
import io.github.wulkanowy.data.db.SharedPrefProvider
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.RemoteConfigHelper
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
@ -37,11 +36,10 @@ internal class DataModule {
@Singleton
@Provides
fun provideSdk(chuckerInterceptor: ChuckerInterceptor, remoteConfig: RemoteConfigHelper) =
fun provideSdk(chuckerInterceptor: ChuckerInterceptor) =
Sdk().apply {
androidVersion = android.os.Build.VERSION.RELEASE
buildTag = android.os.Build.MODEL
userAgentTemplate = remoteConfig.userAgentTemplate
setSimpleHttpLogger { Timber.d(it) }
// for debug only
@ -81,31 +79,22 @@ internal class DataModule {
.readTimeout(30, TimeUnit.SECONDS)
.build()
@OptIn(ExperimentalSerializationApi::class)
@Singleton
@Provides
fun provideAdminMessageService(
fun provideRetrofit(
okHttpClient: OkHttpClient,
json: Json,
appInfo: AppInfo
): AdminMessageService = Retrofit.Builder()
): Retrofit = Retrofit.Builder()
.baseUrl(appInfo.messagesBaseUrl)
.client(okHttpClient)
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
.build()
.create()
@Singleton
@Provides
fun provideSchoolsService(
okHttpClient: OkHttpClient,
json: Json,
appInfo: AppInfo,
): SchoolsService = Retrofit.Builder()
.baseUrl(appInfo.schoolsBaseUrl)
.client(okHttpClient)
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
.build()
.create()
fun provideAdminMessageService(retrofit: Retrofit): AdminMessageService = retrofit.create()
@Singleton
@Provides

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,9 +1,8 @@
package io.github.wulkanowy.data.db.dao
import androidx.room.*
import io.github.wulkanowy.data.db.entities.Semester
import androidx.room.OnConflictStrategy.ABORT
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentName
import io.github.wulkanowy.data.db.entities.StudentNickAndAvatar
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import javax.inject.Singleton
@ -12,7 +11,7 @@ import javax.inject.Singleton
@Dao
abstract class StudentDao {
@Insert(onConflict = OnConflictStrategy.ABORT)
@Insert(onConflict = ABORT)
abstract suspend fun insertAll(student: List<Student>): List<Long>
@Delete
@ -21,9 +20,6 @@ abstract class StudentDao {
@Update(entity = Student::class)
abstract suspend fun update(studentNickAndAvatar: StudentNickAndAvatar)
@Update(entity = Student::class)
abstract suspend fun update(studentName: StudentName)
@Query("SELECT * FROM Students WHERE is_current = 1")
abstract suspend fun loadCurrent(): Student?
@ -34,12 +30,12 @@ abstract class StudentDao {
abstract suspend fun loadAll(): List<Student>
@Transaction
@Query("SELECT * FROM Students JOIN Semesters ON Students.student_id = Semesters.student_id AND Students.class_id = Semesters.class_id")
abstract suspend fun loadStudentsWithSemesters(): Map<Student, List<Semester>>
@Query("SELECT * FROM Students")
abstract suspend fun loadStudentsWithSemesters(): List<StudentWithSemesters>
@Transaction
@Query("SELECT * FROM Students JOIN Semesters ON Students.student_id = Semesters.student_id AND Students.class_id = Semesters.class_id WHERE Students.id = :id")
abstract suspend fun loadStudentWithSemestersById(id: Long): Map<Student, List<Semester>>
@Query("SELECT * FROM Students WHERE id = :id")
abstract suspend fun loadStudentWithSemestersById(id: Long): StudentWithSemesters?
@Query("UPDATE Students SET is_current = 1 WHERE id = :id")
abstract suspend fun updateCurrent(id: Long)

View file

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

View file

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

View file

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

View file

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

View file

@ -1,18 +0,0 @@
package io.github.wulkanowy.data.db.entities
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import java.io.Serializable
@Entity
data class StudentName(
@ColumnInfo(name = "student_name")
val studentName: String
) : Serializable {
@PrimaryKey
var id: Long = 0
}

View file

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

View file

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

View file

@ -1,10 +0,0 @@
package io.github.wulkanowy.data.db.migrations
import androidx.room.DeleteColumn
import androidx.room.migration.AutoMigrationSpec
@DeleteColumn(
tableName = "AdminMessages",
columnName = "type",
)
class Migration57 : AutoMigrationSpec

View file

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

View file

@ -1,11 +0,0 @@
package io.github.wulkanowy.data.enums
enum class TimetableGapsMode(val value: String) {
NO_GAPS("no_gaps"),
BETWEEN_LESSONS("between"),
BETWEEN_AND_BEFORE_LESSONS("before_and_between");
companion object {
fun getByValue(value: String) = entries.find { it.value == value } ?: BETWEEN_LESSONS
}
}

View file

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

View file

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

View file

@ -26,7 +26,7 @@ fun List<SdkMessage>.mapToEntities(
messageId = it.id,
correspondents = it.correspondents,
subject = it.subject.trim(),
date = it.date.toInstant(),
date = it.dateZoned.toInstant(),
folderId = it.folderId,
unread = it.unread,
unreadBy = it.unreadBy,
@ -40,6 +40,7 @@ fun List<SdkMessage>.mapToEntities(
fun List<SdkMessageAttachment>.mapToEntities(messageGlobalKey: String) = map {
MessageAttachment(
messageGlobalKey = messageGlobalKey,
realId = it.url.hashCode(),
url = it.url,
filename = it.filename
)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -2,19 +2,17 @@ package io.github.wulkanowy.data.repositories
import android.content.Context
import android.content.SharedPreferences
import androidx.annotation.StringRes
import androidx.core.content.edit
import com.fredporciuncula.flow.preferences.FlowSharedPreferences
import com.fredporciuncula.flow.preferences.Preference
import com.fredporciuncula.flow.preferences.Serializer
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R
import io.github.wulkanowy.data.enums.*
import io.github.wulkanowy.ui.modules.dashboard.DashboardItem
import io.github.wulkanowy.ui.modules.grade.GradeAverageMode
import io.github.wulkanowy.ui.modules.settings.appearance.menuorder.AppMenuItem
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.time.Instant
@ -30,35 +28,29 @@ class PreferencesRepository @Inject constructor(
private val json: Json,
) {
val startMenuIndex: Int
get() = getString(R.string.pref_key_start_menu, R.string.pref_default_startup).toInt()
val isShowPresent: Boolean
get() = getBoolean(
R.string.pref_key_attendance_present,
R.bool.pref_default_attendance_present
)
private val gradeAverageModePref: Preference<GradeAverageMode>
get() = getObjectFlow(
R.string.pref_key_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 gradeAverageMode: GradeAverageMode
get() = GradeAverageMode.getByValue(
getString(
R.string.pref_key_grade_average_mode,
R.string.pref_default_grade_average_mode
)
)
val gradeAverageModeFlow: Flow<GradeAverageMode>
get() = gradeAverageModePref.asFlow()
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 gradeAverageForceCalc: Boolean
get() = getBoolean(
R.string.pref_key_grade_average_force_calc,
R.bool.pref_default_grade_average_force_calc
)
val gradeAverageForceCalcFlow: Flow<Boolean>
get() = gradeAverageForceCalcPref.asFlow()
val gradeExpandMode: GradeExpandMode
get() = GradeExpandMode.getByValue(
getString(
@ -148,24 +140,12 @@ class PreferencesRepository @Inject constructor(
R.string.pref_default_grade_modifier_plus
).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
get() = getString(
R.string.pref_key_grade_modifier_minus,
R.string.pref_default_grade_modifier_minus
).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
get() = getBoolean(
R.string.pref_key_fill_message_content,
@ -194,25 +174,30 @@ class PreferencesRepository @Inject constructor(
)
)
val showTimetableGaps: TimetableGapsMode
get() = TimetableGapsMode.getByValue(
getString(
R.string.pref_key_timetable_show_gaps,
R.string.pref_default_timetable_show_gaps
)
val showTimetableTimers: Boolean
get() = getBoolean(
R.string.pref_key_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
get() = getBoolean(
R.string.pref_key_subjects_without_grades,
R.bool.pref_default_subjects_without_grades
)
val isOptionalArithmeticAverageFlow: Flow<Boolean>
get() = flowSharedPref.getBoolean(
context.getString(R.string.pref_key_optional_arithmetic_average),
context.resources.getBoolean(R.bool.pref_default_optional_arithmetic_average)
).asFlow()
val isOptionalArithmeticAverage: Boolean
get() = getBoolean(
R.string.pref_key_optional_arithmetic_average,
R.bool.pref_default_optional_arithmetic_average
)
var lasSyncDate: Instant?
get() = getLong(R.string.pref_key_last_sync_date, R.string.pref_default_last_sync_date)
@ -330,26 +315,6 @@ class PreferencesRepository @Inject constructor(
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 isIncognitoMode: Boolean
get() = getBoolean(R.string.pref_key_incognito_moge, R.bool.pref_default_incognito_mode)
set(value) = sharedPref.edit {
putBoolean(context.getString(R.string.pref_key_incognito_moge), value)
}
var installationId: String
get() = sharedPref.getString(PREF_KEY_INSTALLATION_ID, null).orEmpty()
private set(value) = sharedPref.edit { putString(PREF_KEY_INSTALLATION_ID, value) }
@ -365,21 +330,6 @@ class PreferencesRepository @Inject constructor(
private fun getLong(id: String, default: Int) =
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: String, default: Int) =
@ -391,7 +341,6 @@ class PreferencesRepository @Inject constructor(
sharedPref.getBoolean(id, context.resources.getBoolean(default))
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_DASHBOARD_ITEMS_POSITION = "dashboard_items_position"
private const val PREF_KEY_IN_APP_REVIEW_COUNT = "in_app_review_count"

View file

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

View file

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

View file

@ -6,17 +6,16 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.data.db.AppDatabase
import io.github.wulkanowy.data.db.dao.SemesterDao
import io.github.wulkanowy.data.db.dao.StudentDao
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentName
import io.github.wulkanowy.data.db.entities.StudentNickAndAvatar
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
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.pojos.RegisterUser
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.DispatchersProvider
import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.security.decrypt
import io.github.wulkanowy.utils.security.encrypt
import kotlinx.coroutines.withContext
@ -30,27 +29,37 @@ class StudentRepository @Inject constructor(
private val studentDb: StudentDao,
private val semesterDb: SemesterDao,
private val sdk: Sdk,
private val appInfo: AppInfo,
private val appDatabase: AppDatabase
) {
suspend fun isStudentSaved() = getSavedStudents(false).isNotEmpty()
suspend fun isCurrentStudentSet() = studentDb.loadCurrent()?.isCurrent ?: false
suspend fun getStudentsApi(
pin: String,
symbol: String,
token: String
): RegisterUser = sdk
.getStudentsFromHebe(token, pin, symbol, "")
.mapToPojo(null)
): List<StudentWithSemesters> =
sdk.getStudentsFromMobileApi(token, pin, symbol, "")
.mapToEntities(colors = appInfo.defaultColorsForAvatar)
suspend fun getStudentsScrapper(
email: String,
password: String,
scrapperBaseUrl: String,
symbol: String
): List<StudentWithSemesters> =
sdk.getStudentsFromScrapper(email, password, scrapperBaseUrl, symbol)
.mapToEntities(password, appInfo.defaultColorsForAvatar)
suspend fun getUserSubjectsFromScrapper(
email: String,
password: String,
scrapperBaseUrl: String,
domainSuffix: String,
symbol: String
): RegisterUser = sdk
.getUserSubjectsFromScrapper(email, password, scrapperBaseUrl, domainSuffix, symbol)
): RegisterUser = sdk.getUserSubjectsFromScrapper(email, password, scrapperBaseUrl, symbol)
.mapToPojo(password)
suspend fun getStudentsHybrid(
@ -58,33 +67,25 @@ class StudentRepository @Inject constructor(
password: String,
scrapperBaseUrl: String,
symbol: String
): RegisterUser = sdk
.getStudentsHybrid(email, password, scrapperBaseUrl, "", symbol)
.mapToPojo(password)
): List<StudentWithSemesters> =
sdk.getStudentsHybrid(email, password, scrapperBaseUrl, "", symbol)
.mapToEntities(password, appInfo.defaultColorsForAvatar)
suspend fun getSavedStudents(decryptPass: Boolean = true): List<StudentWithSemesters> {
return studentDb.loadStudentsWithSemesters().map { (student, semesters) ->
StudentWithSemesters(
student = student.apply {
if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE) {
suspend fun getSavedStudents(decryptPass: Boolean = true) =
studentDb.loadStudentsWithSemesters()
.map {
it.apply {
if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API) {
student.password = withContext(dispatchers.io) {
decrypt(student.password)
}
}
},
semesters = semesters,
)
}
}
}
}
suspend fun getSavedStudentById(id: Long, decryptPass: Boolean = true): StudentWithSemesters? =
studentDb.loadStudentWithSemestersById(id).let { res ->
StudentWithSemesters(
student = res.keys.firstOrNull() ?: return null,
semesters = res.values.first(),
)
}.apply {
if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE) {
suspend fun getSavedStudentById(id: Long, decryptPass: Boolean = true) =
studentDb.loadStudentWithSemestersById(id)?.apply {
if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API) {
student.password = withContext(dispatchers.io) {
decrypt(student.password)
}
@ -94,7 +95,7 @@ class StudentRepository @Inject constructor(
suspend fun getStudentById(id: Long, decryptPass: Boolean = true): Student {
val student = studentDb.loadById(id) ?: throw NoCurrentStudentException()
if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE) {
if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API) {
student.password = withContext(dispatchers.io) {
decrypt(student.password)
}
@ -105,7 +106,7 @@ class StudentRepository @Inject constructor(
suspend fun getCurrentStudent(decryptPass: Boolean = true): Student {
val student = studentDb.loadCurrent() ?: throw NoCurrentStudentException()
if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE) {
if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API) {
student.password = withContext(dispatchers.io) {
decrypt(student.password)
}
@ -118,7 +119,7 @@ class StudentRepository @Inject constructor(
val students = studentsWithSemesters.map { it.student }
.map {
it.apply {
if (Sdk.Mode.valueOf(it.loginMode) != Sdk.Mode.HEBE) {
if (Sdk.Mode.valueOf(it.loginMode) != Sdk.Mode.API) {
password = withContext(dispatchers.io) {
encrypt(password, context)
}
@ -149,21 +150,4 @@ class StudentRepository @Inject constructor(
suspend fun isOneUniqueStudent() = getSavedStudents(false)
.distinctBy { it.student.studentName }.size == 1
suspend fun authorizePermission(student: Student, semester: Semester, pesel: String) =
sdk.init(student)
.switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear)
.authorizePermission(pesel)
suspend fun refreshStudentName(student: Student, semester: Semester) {
val newCurrentApiStudent = sdk.init(student)
.switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear)
.getCurrentStudent() ?: return
val studentName = StudentName(
studentName = "${newCurrentApiStudent.studentName} ${newCurrentApiStudent.studentSurname}"
).apply { id = student.id }
studentDb.update(studentName)
}
}

View file

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

View file

@ -13,7 +13,6 @@ import io.github.wulkanowy.utils.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.sync.Mutex
import java.time.Instant
import java.time.LocalDate
import javax.inject.Inject
import javax.inject.Singleton
@ -66,7 +65,7 @@ class TimetableRepository @Inject constructor(
fetch = {
val timetableFull = sdk.init(student)
.switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear)
.getTimetable(start.monday, end.sunday)
.getTimetableFull(start.monday, end.sunday)
timetableFull.mapToEntities(semester)
},
@ -165,11 +164,6 @@ class TimetableRepository @Inject constructor(
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>) =
timetableAdditionalDb.insertAll(additionalList)

View file

@ -1,64 +0,0 @@
package io.github.wulkanowy.domain.adminmessage
import io.github.wulkanowy.data.Resource
import io.github.wulkanowy.data.db.entities.AdminMessage
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.enums.MessageType
import io.github.wulkanowy.data.mapResourceData
import io.github.wulkanowy.data.repositories.AdminMessageRepository
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.utils.AppInfo
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
class GetAppropriateAdminMessageUseCase @Inject constructor(
private val adminMessageRepository: AdminMessageRepository,
private val preferencesRepository: PreferencesRepository,
private val appInfo: AppInfo
) {
operator fun invoke(student: Student, type: MessageType): Flow<Resource<AdminMessage?>> {
return invoke(student.scrapperBaseUrl, type)
}
operator fun invoke(scrapperBaseUrl: String, type: MessageType): Flow<Resource<AdminMessage?>> {
return adminMessageRepository.getAdminMessages().mapResourceData { adminMessages ->
adminMessages
.asSequence()
.filter { it.isNotDismissed() }
.filter { it.isVersionMatch() }
.filter { it.isRegisterHostMatch(scrapperBaseUrl) }
.filter { it.isFlavorMatch() }
.filter { it.isTypeMatch(type) }
.maxByOrNull { it.id }
}
}
private fun AdminMessage.isNotDismissed(): Boolean {
return id !in preferencesRepository.dismissedAdminMessageIds
}
private fun AdminMessage.isRegisterHostMatch(scrapperBaseUrl: String): Boolean {
return targetRegisterHost?.let {
scrapperBaseUrl.contains(it, true)
} ?: true
}
private fun AdminMessage.isFlavorMatch(): Boolean {
return targetFlavor?.equals(appInfo.buildFlavor, true) ?: true
}
private fun AdminMessage.isVersionMatch(): Boolean {
val isCorrectMaxVersion = versionMax?.let { it >= appInfo.versionCode } ?: true
val isCorrectMinVersion = versionMin?.let { it <= appInfo.versionCode } ?: true
return isCorrectMaxVersion && isCorrectMinVersion
}
private fun AdminMessage.isTypeMatch(messageType: MessageType): Boolean {
if (messageType in types) return true
if (MessageType.GENERAL_MESSAGE in types) return true
return false
}
}

View file

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

View file

@ -25,21 +25,13 @@ class TimetableWidgetService : RemoteViewsService() {
lateinit var semesterRepo: SemesterRepository
@Inject
lateinit var sharedPref: SharedPrefProvider
lateinit var prefRepository: PreferencesRepository
@Inject
lateinit var prefRepository: PreferencesRepository
lateinit var sharedPref: SharedPrefProvider
override fun onGetViewFactory(intent: Intent?): RemoteViewsFactory {
Timber.d("TimetableWidgetFactory created")
return TimetableWidgetFactory(
timetableRepository = timetableRepo,
studentRepository = studentRepo,
semesterRepository = semesterRepo,
sharedPref = sharedPref,
prefRepository = prefRepository,
context = applicationContext,
intent = intent,
)
return TimetableWidgetFactory(timetableRepo, studentRepo, semesterRepo, prefRepository, sharedPref, applicationContext, intent)
}
}

View file

@ -4,13 +4,12 @@ import android.app.ActivityManager
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
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.LENGTH_LONG
import io.github.wulkanowy.R
import io.github.wulkanowy.ui.modules.auth.AuthDialog
import io.github.wulkanowy.ui.modules.login.LoginActivity
import io.github.wulkanowy.utils.FragmentLifecycleLogger
import io.github.wulkanowy.utils.getThemeAttrColor
@ -31,8 +30,6 @@ abstract class BaseActivity<T : BasePresenter<out BaseView>, VB : ViewBinding> :
protected var messageContainer: View? = null
protected var messageAnchor: View? = null
abstract var presenter: T
override fun onCreate(savedInstanceState: Bundle?) {
@ -51,7 +48,6 @@ abstract class BaseActivity<T : BasePresenter<out BaseView>, VB : ViewBinding> :
if (messageContainer != null) {
Snackbar.make(messageContainer!!, text, LENGTH_LONG)
.setAction(R.string.all_details) { showErrorDetailsDialog(error) }
.apply { messageAnchor?.let { anchorView = it } }
.show()
} else showMessage(text)
}
@ -61,15 +57,12 @@ abstract class BaseActivity<T : BasePresenter<out BaseView>, VB : ViewBinding> :
}
override fun showMessage(text: String) {
if (messageContainer != null) {
Snackbar.make(messageContainer!!, text, LENGTH_LONG)
.apply { messageAnchor?.let { anchorView = it } }
.show()
} else Toast.makeText(this, text, Toast.LENGTH_LONG).show()
if (messageContainer != null) Snackbar.make(messageContainer!!, text, LENGTH_LONG).show()
else Toast.makeText(this, text, Toast.LENGTH_LONG).show()
}
override fun showExpiredDialog() {
MaterialAlertDialogBuilder(this)
AlertDialog.Builder(this)
.setTitle(R.string.main_session_expired)
.setMessage(R.string.main_session_relogin)
.setPositiveButton(R.string.main_log_in) { _, _ -> presenter.onExpiredLoginSelected() }
@ -77,15 +70,10 @@ abstract class BaseActivity<T : BasePresenter<out BaseView>, VB : ViewBinding> :
.show()
}
override fun showAuthDialog() {
AuthDialog.newInstance().show(supportFragmentManager, "auth_dialog")
}
override fun showChangePasswordSnackbar(redirectUrl: String) {
messageContainer?.let {
Snackbar.make(it, R.string.error_password_change_required, LENGTH_LONG)
.setAction(R.string.all_change) { openInternetBrowser(redirectUrl) }
.apply { messageAnchor?.let { anchorView = it } }
.show()
}
}

View file

@ -1,14 +1,8 @@
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 androidx.fragment.app.DialogFragment
import androidx.viewbinding.ViewBinding
import com.google.android.material.elevation.SurfaceColors
import io.github.wulkanowy.ui.modules.auth.AuthDialog
import io.github.wulkanowy.utils.AnalyticsHelper
import io.github.wulkanowy.utils.lifecycleAwareVariable
import javax.inject.Inject
@ -40,25 +34,10 @@ abstract class BaseDialogFragment<VB : ViewBinding> : DialogFragment(), BaseView
(activity as? BaseActivity<*, *>)?.showChangePasswordSnackbar(redirectUrl)
}
override fun showAuthDialog() {
AuthDialog.newInstance().show(childFragmentManager, "auth_dialog")
}
override fun showErrorDetailsDialog(error: Throwable) {
ErrorDialog.newInstance(error).show(childFragmentManager, error.toString())
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
view.setBackgroundColor(SurfaceColors.SURFACE_3.getColor(requireContext()))
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
) = binding.root
override fun onResume() {
super.onResume()
analyticsHelper.setCurrentScreen(requireActivity(), this::class.simpleName)

View file

@ -7,7 +7,6 @@ import androidx.viewbinding.ViewBinding
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.snackbar.Snackbar.LENGTH_LONG
import io.github.wulkanowy.R
import io.github.wulkanowy.ui.modules.auth.AuthDialog
import io.github.wulkanowy.utils.lifecycleAwareVariable
abstract class BaseFragment<VB : ViewBinding>(@LayoutRes layoutId: Int) : Fragment(layoutId),
@ -43,10 +42,6 @@ abstract class BaseFragment<VB : ViewBinding>(@LayoutRes layoutId: Int) : Fragme
(activity as? BaseActivity<*, *>)?.showExpiredDialog()
}
override fun showAuthDialog() {
AuthDialog.newInstance().show(childFragmentManager, "auth_dialog")
}
override fun openClearLoginView() {
(activity as? BaseActivity<*, *>)?.openClearLoginView()
}

View file

@ -1,15 +1,10 @@
package io.github.wulkanowy.ui.base
import io.github.wulkanowy.data.repositories.StudentRepository
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.launch
import timber.log.Timber
open class BasePresenter<T : BaseView>(
@ -31,7 +26,6 @@ open class BasePresenter<T : BaseView>(
onSessionExpired = view::showExpiredDialog
onNoCurrentStudent = view::openClearLoginView
onPasswordChangeRequired = view::showChangePasswordSnackbar
onAuthorizationRequired = view::showAuthDialog
}
}

View file

@ -8,8 +8,6 @@ interface BaseView {
fun showExpiredDialog()
fun showAuthDialog()
fun openClearLoginView()
fun showErrorDetailsDialog(error: Throwable)

View file

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

View file

@ -3,7 +3,6 @@ package io.github.wulkanowy.ui.base
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.data.exceptions.NoCurrentStudentException
import io.github.wulkanowy.sdk.scrapper.exception.AuthorizationRequiredException
import io.github.wulkanowy.sdk.scrapper.login.BadCredentialsException
import io.github.wulkanowy.sdk.scrapper.login.PasswordChangeRequiredException
import io.github.wulkanowy.utils.getErrorString
@ -21,8 +20,6 @@ open class ErrorHandler @Inject constructor(@ApplicationContext protected val co
var onPasswordChangeRequired: (String) -> Unit = {}
var onAuthorizationRequired: () -> Unit = {}
fun dispatch(error: Throwable) {
Timber.e(error, "An exception occurred while the Wulkanowy was running")
proceed(error)
@ -34,7 +31,6 @@ open class ErrorHandler @Inject constructor(@ApplicationContext protected val co
is PasswordChangeRequiredException -> onPasswordChangeRequired(error.redirectUrl)
is ScramblerException, is BadCredentialsException -> onSessionExpired()
is NoCurrentStudentException -> onNoCurrentStudent()
is AuthorizationRequiredException -> onAuthorizationRequired()
}
}
@ -43,6 +39,5 @@ open class ErrorHandler @Inject constructor(@ApplicationContext protected val co
onSessionExpired = {}
onNoCurrentStudent = {}
onPasswordChangeRequired = {}
onAuthorizationRequired = {}
}
}

View file

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

View file

@ -9,14 +9,11 @@ import io.github.wulkanowy.ui.modules.exam.ExamFragment
import io.github.wulkanowy.ui.modules.grade.GradeFragment
import io.github.wulkanowy.ui.modules.homework.HomeworkFragment
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.mobiledevice.MobileDeviceFragment
import io.github.wulkanowy.ui.modules.more.MoreFragment
import io.github.wulkanowy.ui.modules.note.NoteFragment
import io.github.wulkanowy.ui.modules.schoolandteachers.SchoolAndTeachersFragment
import io.github.wulkanowy.ui.modules.schoolandteachers.school.SchoolFragment
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 kotlinx.serialization.Serializable
import java.time.LocalDate
@ -42,13 +39,10 @@ sealed class Destination {
NOTE(Note),
CONFERENCE(Conference),
SCHOOL_ANNOUNCEMENT(SchoolAnnouncement),
SCHOOL_AND_TEACHERS(SchoolAndTeachers),
LUCKY_NUMBER(LuckyNumber),
LUCKY_NUMBER_HISTORY(LuckyNumberHistory),
SCHOOL(School),
LUCKY_NUMBER(More),
MORE(More),
MESSAGE(Message),
MOBILE_DEVICE(MobileDevice),
SETTINGS(Settings);
MESSAGE(Message);
}
@Serializable
@ -109,9 +103,9 @@ sealed class Destination {
}
@Serializable
object SchoolAndTeachers : Destination() {
override val destinationType get() = Type.SCHOOL_AND_TEACHERS
override val destinationFragment get() = SchoolAndTeachersFragment.newInstance()
object School : Destination() {
override val destinationType get() = Type.SCHOOL
override val destinationFragment get() = SchoolFragment.newInstance()
}
@Serializable
@ -120,12 +114,6 @@ sealed class Destination {
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
object More : Destination() {
override val destinationType get() = Type.MORE
@ -137,16 +125,4 @@ sealed class Destination {
override val destinationType get() = Type.MESSAGE
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,7 +9,6 @@ import androidx.appcompat.app.AlertDialog
import androidx.core.os.bundleOf
import androidx.core.view.get
import androidx.core.view.isVisible
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Student
@ -115,7 +114,7 @@ class AccountDetailsFragment :
override fun showLogoutConfirmDialog() {
context?.let {
MaterialAlertDialogBuilder(it)
AlertDialog.Builder(it)
.setTitle(R.string.account_logout_student)
.setMessage(R.string.account_confirm)
.setPositiveButton(R.string.account_logout) { _, _ -> presenter.onLogoutConfirm() }

View file

@ -1,11 +1,11 @@
package io.github.wulkanowy.ui.modules.account.accountedit
import android.app.Dialog
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.recyclerview.widget.GridLayoutManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.databinding.DialogAccountEditBinding
@ -31,13 +31,17 @@ class AccountEditDialog : BaseDialogFragment<DialogAccountEditBinding>(), Accoun
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return MaterialAlertDialogBuilder(requireContext(), theme)
.setView(DialogAccountEditBinding.inflate(layoutInflater).apply { binding = this }.root)
.create()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NO_TITLE, 0)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View = DialogAccountEditBinding.inflate(inflater).apply { binding = this }.root
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
presenter.onAttachView(this, requireArguments().serializable(ARGUMENT_KEY))

View file

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

View file

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

View file

@ -4,10 +4,10 @@ import android.content.DialogInterface.BUTTON_POSITIVE
import android.os.Bundle
import android.view.*
import android.view.View.*
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.view.ActionMode
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Attendance
@ -124,7 +124,7 @@ class AttendanceFragment : BaseFragment<FragmentAttendanceBinding>(R.layout.frag
attendanceExcuseButton.setOnClickListener { presenter.onExcuseButtonClick() }
attendanceNavContainer.elevation = requireContext().dpToPx(3f)
attendanceNavContainer.elevation = requireContext().dpToPx(8f)
}
}
@ -148,10 +148,6 @@ class AttendanceFragment : BaseFragment<FragmentAttendanceBinding>(R.layout.frag
binding.attendanceNavDate.text = date
}
override fun showNavigation(show: Boolean) {
binding.attendanceNavContainer.isVisible = show
}
override fun clearData() {
with(attendanceAdapter) {
items = emptyList()
@ -232,7 +228,7 @@ class AttendanceFragment : BaseFragment<FragmentAttendanceBinding>(R.layout.frag
override fun showExcuseDialog() {
val dialogBinding = DialogExcuseBinding.inflate(LayoutInflater.from(context))
MaterialAlertDialogBuilder(requireContext())
AlertDialog.Builder(requireContext())
.setTitle(R.string.attendance_excuse_title)
.setView(dialogBinding.root)
.setNegativeButton(android.R.string.cancel) { _, _ -> }
@ -285,9 +281,7 @@ class AttendanceFragment : BaseFragment<FragmentAttendanceBinding>(R.layout.frag
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
presenter.currentDate?.let {
outState.putLong(SAVED_DATE_KEY, it.toEpochDay())
}
outState.putLong(SAVED_DATE_KEY, presenter.currentDate.toEpochDay())
}
override fun onDestroyView() {

View file

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

View file

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

View file

@ -1,81 +0,0 @@
package io.github.wulkanowy.ui.modules.auth
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.text.parseAsHtml
import androidx.core.view.isVisible
import androidx.core.widget.doOnTextChanged
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.databinding.DialogAuthBinding
import io.github.wulkanowy.ui.base.BaseDialogFragment
import javax.inject.Inject
@AndroidEntryPoint
class AuthDialog : BaseDialogFragment<DialogAuthBinding>(), AuthView {
@Inject
lateinit var presenter: AuthPresenter
companion object {
fun newInstance() = AuthDialog()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NO_TITLE, R.style.FullScreenDialogStyle)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return DialogAuthBinding.inflate(inflater).apply { binding = this }.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
presenter.onAttachView(this)
binding.authInput.doOnTextChanged { text, _, _, _ ->
presenter.onPeselChange(text?.toString())
}
binding.authButton.setOnClickListener { presenter.authorize() }
binding.authSuccessButton.setOnClickListener {
activity?.recreate()
dismiss()
}
binding.authButtonSkip.setOnClickListener { dismiss() }
}
override fun enableAuthButton(isEnabled: Boolean) {
binding.authButton.isEnabled = isEnabled
}
override fun showProgress(show: Boolean) {
binding.authProgress.isVisible = show
}
override fun showPeselError(show: Boolean) {
binding.authInputLayout.error = getString(R.string.auth_api_error).takeIf { show }
}
override fun showInvalidPeselError(show: Boolean) {
binding.authInputLayout.error = getString(R.string.auth_invalid_error).takeIf { show }
}
override fun showSuccess(show: Boolean) {
binding.authSuccess.isVisible = show
}
override fun showContent(show: Boolean) {
binding.authForm.isVisible = show
}
override fun showDescriptionWithName(name: String) {
binding.authDescription.text = getString(R.string.auth_description, name).parseAsHtml()
}
}

View file

@ -1,100 +0,0 @@
package io.github.wulkanowy.ui.modules.auth
import io.github.wulkanowy.data.repositories.SemesterRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
import kotlinx.coroutines.launch
import javax.inject.Inject
class AuthPresenter @Inject constructor(
private val semesterRepository: SemesterRepository,
errorHandler: ErrorHandler,
studentRepository: StudentRepository
) : BasePresenter<AuthView>(errorHandler, studentRepository) {
private var pesel: String = ""
override fun onAttachView(view: AuthView) {
super.onAttachView(view)
view.enableAuthButton(pesel.length == 11)
view.showSuccess(false)
view.showProgress(false)
loadName()
}
private fun loadName() {
presenterScope.launch {
runCatching { studentRepository.getCurrentStudent(false) }
.onSuccess { view?.showDescriptionWithName(it.studentName) }
.onFailure { errorHandler.dispatch(it) }
}
}
fun onPeselChange(newPesel: String?) {
pesel = newPesel.orEmpty()
view?.enableAuthButton(pesel.length == 11)
view?.showPeselError(false)
view?.showInvalidPeselError(false)
}
fun authorize() {
presenterScope.launch {
view?.showProgress(true)
view?.showContent(false)
if (!isValidPESEL(pesel)) {
view?.showInvalidPeselError(true)
view?.showProgress(false)
view?.showContent(true)
return@launch
}
runCatching {
val student = studentRepository.getCurrentStudent()
val semester = semesterRepository.getCurrentSemester(student)
val isSuccess = studentRepository.authorizePermission(student, semester, pesel)
if (isSuccess) {
studentRepository.refreshStudentName(student, semester)
}
isSuccess
}
.onFailure { errorHandler.dispatch(it) }
.onSuccess {
if (it) {
view?.showSuccess(true)
view?.showContent(false)
view?.showPeselError(false)
} else {
view?.showSuccess(false)
view?.showContent(true)
view?.showPeselError(true)
}
}
view?.showProgress(false)
}
}
private fun isValidPESEL(peselString: String): Boolean {
if (peselString.length != 11) {
return false
}
val weights = intArrayOf(1, 3, 7, 9, 1, 3, 7, 9, 1, 3)
var sum = 0
for (i in 0 until 10) {
sum += weights[i] * Character.getNumericValue(peselString[i])
}
sum %= 10
sum = 10 - sum
sum %= 10
return sum == Character.getNumericValue(peselString[10])
}
}

View file

@ -1,20 +0,0 @@
package io.github.wulkanowy.ui.modules.auth
import io.github.wulkanowy.ui.base.BaseView
interface AuthView : BaseView {
fun enableAuthButton(isEnabled: Boolean)
fun showProgress(show: Boolean)
fun showPeselError(show: Boolean)
fun showInvalidPeselError(show: Boolean)
fun showSuccess(show: Boolean)
fun showContent(show: Boolean)
fun showDescriptionWithName(name: String)
}

View file

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

View file

@ -16,7 +16,7 @@ import javax.inject.Inject
@AndroidEntryPoint
class ConferenceFragment : BaseFragment<FragmentConferenceBinding>(R.layout.fragment_conference),
ConferenceView, MainView.TitledView, MainView.MainChildView {
ConferenceView, MainView.TitledView {
@Inject
lateinit var presenter: ConferencePresenter
@ -109,14 +109,6 @@ class ConferenceFragment : BaseFragment<FragmentConferenceBinding>(R.layout.frag
(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() {
presenter.onDetachView()
super.onDestroyView()

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,52 +0,0 @@
package io.github.wulkanowy.ui.modules.dashboard.viewholders
import android.content.res.ColorStateList
import android.graphics.Color
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.AdminMessage
import io.github.wulkanowy.databinding.ItemDashboardAdminMessageBinding
import io.github.wulkanowy.ui.modules.dashboard.DashboardItem
import io.github.wulkanowy.utils.getThemeAttrColor
class AdminMessageViewHolder(
private val binding: ItemDashboardAdminMessageBinding,
private val onAdminMessageDismissClickListener: (AdminMessage) -> Unit,
private val onAdminMessageClickListener: (String?) -> Unit,
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: AdminMessage?) {
item ?: return
val context = binding.root.context
val (backgroundColor, textColor) = when (item.priority) {
"HIGH" -> {
context.getThemeAttrColor(R.attr.colorMessageHigh) to
context.getThemeAttrColor(R.attr.colorOnMessageHigh)
}
"MEDIUM" -> {
context.getThemeAttrColor(R.attr.colorMessageMedium) to Color.BLACK
}
else -> null to context.getThemeAttrColor(R.attr.colorOnSurface)
}
with(binding) {
dashboardAdminMessageItemTitle.text = item.title
dashboardAdminMessageItemTitle.setTextColor(textColor)
dashboardAdminMessageItemDescription.text = item.content
dashboardAdminMessageItemDescription.setTextColor(textColor)
dashboardAdminMessageItemIcon.setColorFilter(textColor)
dashboardAdminMessageItemDismiss.isVisible = item.isDismissible
dashboardAdminMessageItemDismiss.setTextColor(textColor)
dashboardAdminMessageItemDismiss.setOnClickListener {
onAdminMessageDismissClickListener(item)
}
root.setCardBackgroundColor(backgroundColor?.let { ColorStateList.valueOf(it) })
item.destinationUrl?.let { url ->
root.setOnClickListener { onAdminMessageClickListener(url) }
}
}
}
}

View file

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

View file

@ -2,7 +2,9 @@ package io.github.wulkanowy.ui.modules.exam
import android.os.Bundle
import android.view.View
import android.view.View.*
import android.view.View.GONE
import android.view.View.INVISIBLE
import android.view.View.VISIBLE
import androidx.recyclerview.widget.LinearLayoutManager
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
@ -18,7 +20,7 @@ import javax.inject.Inject
@AndroidEntryPoint
class ExamFragment : BaseFragment<FragmentExamBinding>(R.layout.fragment_exam), ExamView,
MainView.TitledView, MainView.MainChildView {
MainView.TitledView {
@Inject
lateinit var presenter: ExamPresenter
@ -62,7 +64,7 @@ class ExamFragment : BaseFragment<FragmentExamBinding>(R.layout.fragment_exam),
examPreviousButton.setOnClickListener { presenter.onPreviousWeek() }
examNextButton.setOnClickListener { presenter.onNextWeek() }
examNavContainer.elevation = requireContext().dpToPx(3f)
examNavContainer.elevation = requireContext().dpToPx(8f)
}
}
@ -124,14 +126,6 @@ class ExamFragment : BaseFragment<FragmentExamBinding>(R.layout.fragment_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) {
super.onSaveInstanceState(outState)
outState.putLong(SAVED_DATE_KEY, presenter.currentDate.toEpochDay())

View file

@ -175,17 +175,4 @@ 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,6 +34,4 @@ interface ExamView : BaseView {
fun showPreButton(show: Boolean)
fun showExamDialog(exam: Exam)
fun resetView()
}

View file

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

View file

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

View file

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

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