Merge branch 'release/1.1.0'

This commit is contained in:
Mikołaj Pich 2021-03-07 21:58:23 +01:00
commit e15eb03299
179 changed files with 8854 additions and 1609 deletions

View File

@ -10,8 +10,8 @@ on:
workflow_dispatch: workflow_dispatch:
jobs: jobs:
build: unit-tests:
name: Pre-build name: Unit tests
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 10 timeout-minutes: 10
steps: steps:
@ -27,36 +27,6 @@ jobs:
~/.gradle/caches ~/.gradle/caches
~/.gradle/wrapper ~/.gradle/wrapper
key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*') }} key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*') }}
- name: Build
run: ./gradlew --build-cache compileFdroidDebugUnitTestKotlin preFdroidDebugAndroidTestBuild dexBuilderFdroidDebugAndroidTest packageFdroidDebug packageFdroidDebugAndroidTest
- name: Prepare build cache
run: tar -cf prebuild.tar .build-cache .gradle app/build
- uses: actions/upload-artifact@v2
with:
name: prebuild.tar
path: prebuild.tar
unit-tests:
name: Unit tests
runs-on: ubuntu-latest
timeout-minutes: 10
needs: [ build ]
steps:
- uses: actions/checkout@v2
- uses: actions/setup-java@v1
with:
java-version: 11
- uses: actions/cache@v2
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*') }}
- uses: actions/download-artifact@v2
with:
name: prebuild.tar
- name: Extract build cache
run: tar -xf prebuild.tar
- name: Unit tests - name: Unit tests
run: | run: |
./gradlew --build-cache -Pcoverage testFdroidDebugUnitTest --stacktrace ./gradlew --build-cache -Pcoverage testFdroidDebugUnitTest --stacktrace
@ -65,49 +35,12 @@ jobs:
with: with:
flags: unit flags: unit
instrumentation-tests:
name: Instrumentation tests
runs-on: macOS-latest
timeout-minutes: 15
needs: [ build ]
strategy:
fail-fast: true
matrix:
api-level: [21, 29]
steps:
- uses: actions/checkout@v2
- uses: actions/setup-java@v1
with:
java-version: 11
- uses: actions/cache@v2
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*') }}
- uses: actions/download-artifact@v2
with:
name: prebuild.tar
- name: Extract build cache
run: tar -xf prebuild.tar
- name: Instrumentation tests
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ matrix.api-level }}
arch: x86
script: |
./gradlew --build-cache -Pcoverage connectedFdroidDebugAndroidTest --stacktrace
./gradlew --build-cache -Pcoverage jacocoTestReport --stacktrace
- uses: codecov/codecov-action@v1
with:
flags: instrumented,api-${{ matrix.api-level }}
deploy-google-play: deploy-google-play:
name: Deploy to google play name: Deploy to google play
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 10 timeout-minutes: 10
environment: google-play environment: google-play
needs: [ build, unit-tests, instrumentation-tests ] needs: [ unit-tests ]
if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') if: github.event_name == 'push' && contains(github.ref, 'refs/tags/')
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
@ -120,11 +53,6 @@ jobs:
~/.gradle/caches ~/.gradle/caches
~/.gradle/wrapper ~/.gradle/wrapper
key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*') }} key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*') }}
- uses: actions/download-artifact@v2
with:
name: prebuild.tar
- name: Extract build cache
run: tar -xf prebuild.tar
- name: Decrypt keys - name: Decrypt keys
env: env:
ENCRYPT_KEY: ${{ secrets.ENCRYPT_KEY }} ENCRYPT_KEY: ${{ secrets.ENCRYPT_KEY }}

View File

@ -12,7 +12,7 @@ Unofficial android VULCAN UONET+ register client for both students and their par
## Features ## Features
* logging in using the email and password OR using token and pin * logging in using the email and password
* functions from the register website: * functions from the register website:
* grades * grades
* grade statistics * grade statistics
@ -25,15 +25,19 @@ Unofficial android VULCAN UONET+ register client for both students and their par
* homework * homework
* notes * notes
* lucky number * lucky number
* additional lessons
* school conferences
* student and school information
* calculation of the average independently of school's preferences * calculation of the average independently of school's preferences
* notifications, e.g. about a new grade * notifications, e.g. about a new grade
* support for multiple accounts with the ability to rename students
* dark and black (AMOLED) theme * dark and black (AMOLED) theme
* offline mode * offline mode
* no ads * no ads
## Download ## Download
You can download the current beta version from the Google Play, F-Droid or Huawei AppGallery store You can download the current version from the Google Play, F-Droid or Huawei AppGallery store
[<img src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png" [<img src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png"
alt="Get it on Google Play" alt="Get it on Google Play"
@ -60,6 +64,9 @@ You can also download a [development version](https://wulkanowy.github.io/#downl
Please contribute to the project either by creating a PR or submitting an issue on GitHub. Please contribute to the project either by creating a PR or submitting an issue on GitHub.
For people interested in translating the application into different languages, we provide Crowdin
https://crowdin.com/project/wulkanowy2
## License ## License
This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details

View File

@ -12,7 +12,7 @@ Nieoficjalny klient dziennika VULCAN UONET+ dla ucznia i rodzica
## Funkcje ## Funkcje
* logowanie za pomocą e-maila i hasła LUB tokena i pinu * logowanie za pomocą e-maila i hasła
* funkcje ze strony internetowej dziennika: * funkcje ze strony internetowej dziennika:
* oceny * oceny
* statystyki ocen * statystyki ocen
@ -25,15 +25,19 @@ Nieoficjalny klient dziennika VULCAN UONET+ dla ucznia i rodzica
* zadania domowe * zadania domowe
* uwagi * uwagi
* szczęśliwy numerek * szczęśliwy numerek
* dodatkowe lekcje
* zebrania w szkole
* informacje o uczniu i szkole
* obliczanie średniej niezależnie od preferencji szkoły * obliczanie średniej niezależnie od preferencji szkoły
* powiadomienia np. o nowej ocenie * powiadomienia np. o nowej ocenie
* obsługa wielu kont wraz z możliwością zmiany nazwy ucznia
* ciemny i czarny (AMOLED) motyw * ciemny i czarny (AMOLED) motyw
* tryb offilne * tryb offilne
* brak reklam * brak reklam
## Pobierz ## Pobierz
Aktualną wersję beta możesz pobrać ze sklepu Google Play, F-Droid lub Huawei AppGallery Aktualną wersję możesz pobrać ze sklepu Google Play, F-Droid lub Huawei AppGallery
[<img src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png" [<img src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png"
alt="Pobierz z Google Play" alt="Pobierz z Google Play"
@ -61,6 +65,9 @@ Możesz także pobrać [wersję rozwojową](https://wulkanowy.github.io/#downloa
Wnieś swój wkład w projekt, tworząc PR lub wysyłając issue na GitHub. Wnieś swój wkład w projekt, tworząc PR lub wysyłając issue na GitHub.
Dla osób zainteresowanych tłumaczeniem aplikacji na różne języki udostępniamy Crowdina
https://crowdin.com/project/wulkanowy2
## Licencja ## Licencja
Ten projekt udostępniany jest na licencji Apache License 2.0 - szczegóły w pliku [LICENSE](LICENSE) Ten projekt udostępniany jest na licencji Apache License 2.0 - szczegóły w pliku [LICENSE](LICENSE)

View File

@ -18,8 +18,8 @@ android {
testApplicationId "io.github.tests.wulkanowy" testApplicationId "io.github.tests.wulkanowy"
minSdkVersion 17 minSdkVersion 17
targetSdkVersion 30 targetSdkVersion 30
versionCode 84 versionCode 86
versionName "1.0.0" versionName "1.1.0"
multiDexEnabled true multiDexEnabled true
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true
@ -41,7 +41,8 @@ android {
} }
sourceSets { sourceSets {
androidTest.assets.srcDirs += files("$projectDir/schemas".toString()) // https://github.com/robolectric/robolectric/issues/3928#issuecomment-395309991
debug.assets.srcDirs += files("$projectDir/schemas".toString())
} }
signingConfigs { signingConfigs {
@ -103,6 +104,10 @@ android {
disable 'HardwareIds' disable 'HardwareIds'
} }
testOptions.unitTests {
includeAndroidResources = true
}
compileOptions { compileOptions {
coreLibraryDesugaringEnabled true coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
@ -128,32 +133,32 @@ play {
serviceAccountEmail = System.getenv("PLAY_SERVICE_ACCOUNT_EMAIL") ?: "jan@fakelog.cf" serviceAccountEmail = System.getenv("PLAY_SERVICE_ACCOUNT_EMAIL") ?: "jan@fakelog.cf"
serviceAccountCredentials = file('key.p12') serviceAccountCredentials = file('key.p12')
defaultToAppBundles = false defaultToAppBundles = false
track = 'alpha' track = 'production'
updatePriority = 3 updatePriority = 3
} }
ext { ext {
work_manager = "2.5.0" work_manager = "2.5.0"
work_hilt = "1.0.0-alpha03" work_hilt = "1.0.0-alpha03"
room = "2.3.0-beta01" room = "2.3.0-beta02"
chucker = "3.4.0" chucker = "3.4.0"
mockk = "1.10.5" mockk = "1.10.6"
moshi = "1.11.0" moshi = "1.11.0"
} }
dependencies { dependencies {
implementation "io.github.wulkanowy:sdk:1.0.0" implementation "io.github.wulkanowy:sdk:1.1.0"
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.1' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.2' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.2'
implementation "androidx.core:core-ktx:1.3.2" implementation "androidx.core:core-ktx:1.3.2"
implementation "androidx.activity:activity-ktx:1.1.0" implementation "androidx.activity:activity-ktx:1.2.0"
implementation "androidx.appcompat:appcompat:1.2.0" implementation "androidx.appcompat:appcompat:1.2.0"
implementation "androidx.appcompat:appcompat-resources:1.2.0" implementation "androidx.appcompat:appcompat-resources:1.2.0"
implementation "androidx.fragment:fragment-ktx:1.2.5" implementation "androidx.fragment:fragment-ktx:1.3.0"
implementation "androidx.annotation:annotation:1.1.0" implementation "androidx.annotation:annotation:1.1.0"
implementation "androidx.multidex:multidex:2.0.1" implementation "androidx.multidex:multidex:2.0.1"
@ -163,14 +168,15 @@ dependencies {
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
implementation "androidx.constraintlayout:constraintlayout:2.0.4" implementation "androidx.constraintlayout:constraintlayout:2.0.4"
implementation "androidx.coordinatorlayout:coordinatorlayout:1.1.0" implementation "androidx.coordinatorlayout:coordinatorlayout:1.1.0"
implementation "com.google.android.material:material:1.3.0-rc01" implementation "com.google.android.material:material:1.3.0"
implementation "com.github.wulkanowy:material-chips-input:2.1.1" implementation "com.github.wulkanowy:material-chips-input:2.1.1"
implementation "com.github.PhilJay:MPAndroidChart:v3.1.0" implementation "com.github.PhilJay:MPAndroidChart:v3.1.0"
implementation 'com.mikhaellopez:circularimageview:4.2.0'
implementation "androidx.work:work-runtime-ktx:$work_manager" implementation "androidx.work:work-runtime-ktx:$work_manager"
playImplementation "androidx.work:work-gcm:$work_manager" playImplementation "androidx.work:work-gcm:$work_manager"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0" implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.0"
implementation "androidx.room:room-runtime:$room" implementation "androidx.room:room-runtime:$room"
implementation "androidx.room:room-ktx:$room" implementation "androidx.room:room-ktx:$room"
@ -197,7 +203,7 @@ dependencies {
implementation "io.github.wulkanowy:AppKillerManager:3.0.0" implementation "io.github.wulkanowy:AppKillerManager:3.0.0"
implementation 'me.xdrop:fuzzywuzzy:1.3.1' implementation 'me.xdrop:fuzzywuzzy:1.3.1'
playImplementation platform('com.google.firebase:firebase-bom:26.4.0') playImplementation platform('com.google.firebase:firebase-bom:26.6.0')
playImplementation 'com.google.firebase:firebase-analytics-ktx' playImplementation 'com.google.firebase:firebase-analytics-ktx'
playImplementation 'com.google.firebase:firebase-inappmessaging-display-ktx' playImplementation 'com.google.firebase:firebase-inappmessaging-display-ktx'
playImplementation "com.google.firebase:firebase-inappmessaging-ktx" playImplementation "com.google.firebase:firebase-inappmessaging-ktx"
@ -206,24 +212,31 @@ dependencies {
playImplementation 'com.google.android.play:core-ktx:1.8.1' playImplementation 'com.google.android.play:core-ktx:1.8.1'
playImplementation 'com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava' playImplementation 'com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava'
hmsImplementation 'com.huawei.hms:hianalytics:5.1.0.301' hmsImplementation 'com.huawei.hms:hianalytics:5.2.0.300'
hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.5.0.200' hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.5.0.300'
releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker" releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker"
debugImplementation "com.github.ChuckerTeam.Chucker:library:$chucker" debugImplementation "com.github.ChuckerTeam.Chucker:library:$chucker"
debugImplementation "com.amitshekhar.android:debug-db:1.0.6" debugImplementation "com.amitshekhar.android:debug-db:1.0.6"
testImplementation "junit:junit:4.13.1" testImplementation "junit:junit:4.13.2"
testImplementation "io.mockk:mockk:$mockk" testImplementation "io.mockk:mockk:$mockk"
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.4.2' testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.4.2'
testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version" testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
testImplementation 'org.robolectric:robolectric:4.5.1'
testImplementation "androidx.test:runner:1.3.0"
testImplementation "androidx.test.ext:junit:1.1.2"
testImplementation "androidx.test:core:1.3.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.3.0" androidTestImplementation "androidx.test:core:1.3.0"
androidTestImplementation "androidx.test:runner:1.3.0" androidTestImplementation "androidx.test:runner:1.3.0"
androidTestImplementation "androidx.test.ext:junit:1.1.2" androidTestImplementation "androidx.test.ext:junit:1.1.2"
androidTestImplementation "io.mockk:mockk-android:$mockk" androidTestImplementation "io.mockk:mockk-android:$mockk"
androidTestImplementation "androidx.room:room-testing:$room"
androidTestImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version" androidTestImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -38,5 +38,13 @@
{ {
"displayName": "MRmlik12", "displayName": "MRmlik12",
"githubUsername": "MRmlik12" "githubUsername": "MRmlik12"
},
{
"displayName": "Damian Czupryn",
"githubUsername": "Daxxxis"
},
{
"displayName": "Kamil Studziński",
"githubUsername": "studzinskik"
} }
] ]

View File

@ -1,11 +1,13 @@
package io.github.wulkanowy package io.github.wulkanowy
import android.annotation.SuppressLint
import android.app.Application import android.app.Application
import android.content.Context import android.content.Context
import android.util.Log.DEBUG import android.util.Log.DEBUG
import android.util.Log.INFO import android.util.Log.INFO
import android.util.Log.VERBOSE import android.util.Log.VERBOSE
import android.webkit.WebView import android.webkit.WebView
import androidx.fragment.app.FragmentManager
import androidx.hilt.work.HiltWorkerFactory import androidx.hilt.work.HiltWorkerFactory
import androidx.multidex.MultiDex import androidx.multidex.MultiDex
import androidx.work.Configuration import androidx.work.Configuration
@ -46,9 +48,10 @@ class WulkanowyApp : Application(), Configuration.Provider {
MultiDex.install(this) MultiDex.install(this)
} }
@SuppressLint("UnsafeOptInUsageWarning")
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
FragmentManager.enableNewStateManager(false)
initializeAppLanguage() initializeAppLanguage()
themeManager.applyDefaultTheme() themeManager.applyDefaultTheme()
initLogging() initLogging()

View File

@ -17,6 +17,7 @@ import io.github.wulkanowy.data.db.AppDatabase
import io.github.wulkanowy.data.db.SharedPrefProvider import io.github.wulkanowy.data.db.SharedPrefProvider
import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.AppInfo
import timber.log.Timber import timber.log.Timber
import javax.inject.Singleton import javax.inject.Singleton
@ -60,7 +61,8 @@ internal class RepositoryModule {
fun provideDatabase( fun provideDatabase(
@ApplicationContext context: Context, @ApplicationContext context: Context,
sharedPrefProvider: SharedPrefProvider, sharedPrefProvider: SharedPrefProvider,
) = AppDatabase.newInstance(context, sharedPrefProvider) appInfo: AppInfo
) = AppDatabase.newInstance(context, sharedPrefProvider, appInfo)
@Singleton @Singleton
@Provides @Provides

View File

@ -6,7 +6,6 @@ import androidx.room.Room
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import androidx.room.RoomDatabase.JournalMode.TRUNCATE import androidx.room.RoomDatabase.JournalMode.TRUNCATE
import androidx.room.TypeConverters import androidx.room.TypeConverters
import androidx.room.migration.Migration
import io.github.wulkanowy.data.db.dao.AttendanceDao import io.github.wulkanowy.data.db.dao.AttendanceDao
import io.github.wulkanowy.data.db.dao.AttendanceSummaryDao import io.github.wulkanowy.data.db.dao.AttendanceSummaryDao
import io.github.wulkanowy.data.db.dao.CompletedLessonsDao import io.github.wulkanowy.data.db.dao.CompletedLessonsDao
@ -85,12 +84,15 @@ import io.github.wulkanowy.data.db.migrations.Migration30
import io.github.wulkanowy.data.db.migrations.Migration31 import io.github.wulkanowy.data.db.migrations.Migration31
import io.github.wulkanowy.data.db.migrations.Migration32 import io.github.wulkanowy.data.db.migrations.Migration32
import io.github.wulkanowy.data.db.migrations.Migration33 import io.github.wulkanowy.data.db.migrations.Migration33
import io.github.wulkanowy.data.db.migrations.Migration34
import io.github.wulkanowy.data.db.migrations.Migration35
import io.github.wulkanowy.data.db.migrations.Migration4 import io.github.wulkanowy.data.db.migrations.Migration4
import io.github.wulkanowy.data.db.migrations.Migration5 import io.github.wulkanowy.data.db.migrations.Migration5
import io.github.wulkanowy.data.db.migrations.Migration6 import io.github.wulkanowy.data.db.migrations.Migration6
import io.github.wulkanowy.data.db.migrations.Migration7 import io.github.wulkanowy.data.db.migrations.Migration7
import io.github.wulkanowy.data.db.migrations.Migration8 import io.github.wulkanowy.data.db.migrations.Migration8
import io.github.wulkanowy.data.db.migrations.Migration9 import io.github.wulkanowy.data.db.migrations.Migration9
import io.github.wulkanowy.utils.AppInfo
import javax.inject.Singleton import javax.inject.Singleton
@Singleton @Singleton
@ -130,10 +132,9 @@ import javax.inject.Singleton
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
companion object { companion object {
const val VERSION_SCHEMA = 33 const val VERSION_SCHEMA = 35
fun getMigrations(sharedPrefProvider: SharedPrefProvider): Array<Migration> { fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf(
return arrayOf(
Migration2(), Migration2(),
Migration3(), Migration3(),
Migration4(), Migration4(),
@ -165,19 +166,22 @@ abstract class AppDatabase : RoomDatabase() {
Migration30(), Migration30(),
Migration31(), Migration31(),
Migration32(), Migration32(),
Migration33() Migration33(),
Migration34(),
Migration35(appInfo)
) )
}
fun newInstance(context: Context, sharedPrefProvider: SharedPrefProvider): AppDatabase { fun newInstance(
return Room.databaseBuilder(context, AppDatabase::class.java, "wulkanowy_database") context: Context,
sharedPrefProvider: SharedPrefProvider,
appInfo: AppInfo
) = Room.databaseBuilder(context, AppDatabase::class.java, "wulkanowy_database")
.setJournalMode(TRUNCATE) .setJournalMode(TRUNCATE)
.fallbackToDestructiveMigrationFrom(VERSION_SCHEMA + 1) .fallbackToDestructiveMigrationFrom(VERSION_SCHEMA + 1)
.fallbackToDestructiveMigrationOnDowngrade() .fallbackToDestructiveMigrationOnDowngrade()
.addMigrations(*getMigrations(sharedPrefProvider)) .addMigrations(*getMigrations(sharedPrefProvider, appInfo))
.build() .build()
} }
}
abstract val studentDao: StudentDao abstract val studentDao: StudentDao

View File

@ -13,4 +13,7 @@ interface LuckyNumberDao : BaseDao<LuckyNumber> {
@Query("SELECT * FROM LuckyNumbers WHERE student_id = :studentId AND date = :date") @Query("SELECT * FROM LuckyNumbers WHERE student_id = :studentId AND date = :date")
fun load(studentId: Int, date: LocalDate): Flow<LuckyNumber?> fun load(studentId: Int, date: LocalDate): Flow<LuckyNumber?>
@Query("SELECT * FROM LuckyNumbers WHERE student_id = :studentId AND date >= :start AND date <= :end")
fun getAll(studentId: Int, start: LocalDate, end: LocalDate): Flow<List<LuckyNumber>>
} }

View File

@ -8,7 +8,7 @@ import androidx.room.Query
import androidx.room.Transaction import androidx.room.Transaction
import androidx.room.Update import androidx.room.Update
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentNick import io.github.wulkanowy.data.db.entities.StudentNickAndAvatar
import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import javax.inject.Singleton import javax.inject.Singleton
@ -23,13 +23,13 @@ interface StudentDao {
suspend fun delete(student: Student) suspend fun delete(student: Student)
@Update(entity = Student::class) @Update(entity = Student::class)
suspend fun update(studentNick: StudentNick) suspend fun update(studentNickAndAvatar: StudentNickAndAvatar)
@Query("SELECT * FROM Students WHERE is_current = 1") @Query("SELECT * FROM Students WHERE is_current = 1")
suspend fun loadCurrent(): Student? suspend fun loadCurrent(): Student?
@Query("SELECT * FROM Students WHERE id = :id") @Query("SELECT * FROM Students WHERE id = :id")
suspend fun loadById(id: Int): Student? suspend fun loadById(id: Long): Student?
@Query("SELECT * FROM Students") @Query("SELECT * FROM Students")
suspend fun loadAll(): List<Student> suspend fun loadAll(): List<Student>

View File

@ -10,7 +10,7 @@ import java.time.LocalDateTime
data class Message( data class Message(
@ColumnInfo(name = "student_id") @ColumnInfo(name = "student_id")
val studentId: Int, val studentId: Long,
@ColumnInfo(name = "real_id") @ColumnInfo(name = "real_id")
val realId: Int, val realId: Int,

View File

@ -81,4 +81,7 @@ data class Student(
var id: Long = 0 var id: Long = 0
var nick = "" var nick = ""
@ColumnInfo(name = "avatar_color")
var avatarColor = 0L
} }

View File

@ -1,13 +1,17 @@
package io.github.wulkanowy.data.db.entities package io.github.wulkanowy.data.db.entities
import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import java.io.Serializable import java.io.Serializable
@Entity @Entity
data class StudentNick( data class StudentNickAndAvatar(
val nick: String val nick: String,
@ColumnInfo(name = "avatar_color")
var avatarColor: Long
) : Serializable { ) : Serializable {

View File

@ -0,0 +1,13 @@
package io.github.wulkanowy.data.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration34 : Migration(33, 34) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("DELETE FROM ReportingUnits")
database.execSQL("DELETE FROM Recipients")
}
}

View File

@ -0,0 +1,24 @@
package io.github.wulkanowy.data.db.migrations
import androidx.core.database.getLongOrNull
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import io.github.wulkanowy.utils.AppInfo
class Migration35(private val appInfo: AppInfo) : Migration(34, 35) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE Students ADD COLUMN `avatar_color` INTEGER NOT NULL DEFAULT 0")
val studentsCursor = database.query("SELECT * FROM Students")
while (studentsCursor.moveToNext()) {
val studentId = studentsCursor.getLongOrNull(0)
database.execSQL(
"""UPDATE Students
SET avatar_color = ${appInfo.defaultColorsForAvatar.random()}
WHERE id = $studentId"""
)
}
}
}

View File

@ -5,10 +5,9 @@ import io.github.wulkanowy.data.db.entities.GradePointsStatistics
import io.github.wulkanowy.data.db.entities.GradeSemesterStatistics import io.github.wulkanowy.data.db.entities.GradeSemesterStatistics
import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.pojos.GradeStatisticsItem import io.github.wulkanowy.data.pojos.GradeStatisticsItem
import io.github.wulkanowy.ui.modules.grade.statistics.ViewType
import io.github.wulkanowy.sdk.pojo.GradeStatisticsSubject as SdkGradeStatisticsSubject
import io.github.wulkanowy.sdk.pojo.GradeStatisticsSemester as SdkGradeStatisticsSemester
import io.github.wulkanowy.sdk.pojo.GradePointsStatistics as SdkGradePointsStatistics import io.github.wulkanowy.sdk.pojo.GradePointsStatistics as SdkGradePointsStatistics
import io.github.wulkanowy.sdk.pojo.GradeStatisticsSemester as SdkGradeStatisticsSemester
import io.github.wulkanowy.sdk.pojo.GradeStatisticsSubject as SdkGradeStatisticsSubject
@JvmName("mapToEntitiesSubject") @JvmName("mapToEntitiesSubject")
fun List<SdkGradeStatisticsSubject>.mapToEntities(semester: Semester) = map { fun List<SdkGradeStatisticsSubject>.mapToEntities(semester: Semester) = map {
@ -51,7 +50,7 @@ fun List<SdkGradePointsStatistics>.mapToEntities(semester: Semester) = map {
fun List<GradePartialStatistics>.mapPartialToStatisticItems() = filterNot { it.classAmounts.isEmpty() }.map { fun List<GradePartialStatistics>.mapPartialToStatisticItems() = filterNot { it.classAmounts.isEmpty() }.map {
GradeStatisticsItem( GradeStatisticsItem(
type = ViewType.PARTIAL, type = GradeStatisticsItem.DataType.PARTIAL,
average = it.classAverage, average = it.classAverage,
partial = it, partial = it,
points = null, points = null,
@ -61,7 +60,7 @@ fun List<GradePartialStatistics>.mapPartialToStatisticItems() = filterNot { it.c
fun List<GradeSemesterStatistics>.mapSemesterToStatisticItems() = filterNot { it.amounts.isEmpty() }.map { fun List<GradeSemesterStatistics>.mapSemesterToStatisticItems() = filterNot { it.amounts.isEmpty() }.map {
GradeStatisticsItem( GradeStatisticsItem(
type = ViewType.SEMESTER, type = GradeStatisticsItem.DataType.SEMESTER,
partial = null, partial = null,
points = null, points = null,
average = "", average = "",
@ -71,7 +70,7 @@ fun List<GradeSemesterStatistics>.mapSemesterToStatisticItems() = filterNot { it
fun List<GradePointsStatistics>.mapPointsToStatisticsItems() = map { fun List<GradePointsStatistics>.mapPointsToStatisticsItems() = map {
GradeStatisticsItem( GradeStatisticsItem(
type = ViewType.POINTS, type = GradeStatisticsItem.DataType.POINTS,
partial = null, partial = null,
semester = null, semester = null,
average = "", average = "",

View File

@ -4,14 +4,14 @@ import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.MessageAttachment import io.github.wulkanowy.data.db.entities.MessageAttachment
import io.github.wulkanowy.data.db.entities.Recipient import io.github.wulkanowy.data.db.entities.Recipient
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.sdk.pojo.Recipient as SdkRecipient
import io.github.wulkanowy.sdk.pojo.MessageAttachment as SdkMessageAttachment
import java.time.LocalDateTime import java.time.LocalDateTime
import io.github.wulkanowy.sdk.pojo.Message as SdkMessage import io.github.wulkanowy.sdk.pojo.Message as SdkMessage
import io.github.wulkanowy.sdk.pojo.MessageAttachment as SdkMessageAttachment
import io.github.wulkanowy.sdk.pojo.Recipient as SdkRecipient
fun List<SdkMessage>.mapToEntities(student: Student) = map { fun List<SdkMessage>.mapToEntities(student: Student) = map {
Message( Message(
studentId = student.id.toInt(), studentId = student.id,
realId = it.id ?: 0, realId = it.id ?: 0,
messageId = it.messageId ?: 0, messageId = it.messageId ?: 0,
sender = it.sender?.name.orEmpty(), sender = it.sender?.name.orEmpty(),

View File

@ -5,7 +5,7 @@ import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import java.time.LocalDateTime import java.time.LocalDateTime
import io.github.wulkanowy.sdk.pojo.Student as SdkStudent import io.github.wulkanowy.sdk.pojo.Student as SdkStudent
fun List<SdkStudent>.mapToEntities(password: String = "") = map { fun List<SdkStudent>.mapToEntities(password: String = "", colors: List<Long>) = map {
StudentWithSemesters( StudentWithSemesters(
student = Student( student = Student(
email = it.email, email = it.email,
@ -28,8 +28,10 @@ fun List<SdkStudent>.mapToEntities(password: String = "") = map {
mobileBaseUrl = it.mobileBaseUrl, mobileBaseUrl = it.mobileBaseUrl,
privateKey = it.privateKey, privateKey = it.privateKey,
certificateKey = it.certificateKey, certificateKey = it.certificateKey,
loginMode = it.loginMode.name loginMode = it.loginMode.name,
), ).apply {
avatarColor = colors.random()
},
semesters = it.semesters.mapToEntities(it.studentId) semesters = it.semesters.mapToEntities(it.studentId)
) )
} }

View File

@ -3,11 +3,10 @@ package io.github.wulkanowy.data.pojos
import io.github.wulkanowy.data.db.entities.GradePartialStatistics import io.github.wulkanowy.data.db.entities.GradePartialStatistics
import io.github.wulkanowy.data.db.entities.GradePointsStatistics import io.github.wulkanowy.data.db.entities.GradePointsStatistics
import io.github.wulkanowy.data.db.entities.GradeSemesterStatistics import io.github.wulkanowy.data.db.entities.GradeSemesterStatistics
import io.github.wulkanowy.ui.modules.grade.statistics.ViewType
data class GradeStatisticsItem( data class GradeStatisticsItem(
val type: ViewType, val type: DataType,
val average: String, val average: String,
@ -16,4 +15,11 @@ data class GradeStatisticsItem(
val semester: GradeSemesterStatistics?, val semester: GradeSemesterStatistics?,
val points: GradePointsStatistics? val points: GradePointsStatistics?
)
) {
enum class DataType {
SEMESTER,
PARTIAL,
POINTS,
}
}

View File

@ -14,6 +14,7 @@ import io.github.wulkanowy.utils.monday
import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.networkBoundResource
import io.github.wulkanowy.utils.sunday import io.github.wulkanowy.utils.sunday
import io.github.wulkanowy.utils.uniqueSubtract import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.sync.Mutex
import java.time.LocalDate import java.time.LocalDate
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.LocalTime import java.time.LocalTime
@ -27,9 +28,12 @@ class AttendanceRepository @Inject constructor(
private val refreshHelper: AutoRefreshHelper, private val refreshHelper: AutoRefreshHelper,
) { ) {
private val saveFetchResultMutex = Mutex()
private val cacheKey = "attendance" private val cacheKey = "attendance"
fun getAttendance(student: Student, semester: Semester, start: LocalDate, end: LocalDate, forceRefresh: Boolean) = networkBoundResource( fun getAttendance(student: Student, semester: Semester, start: LocalDate, end: LocalDate, forceRefresh: Boolean) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester, start, end)) }, shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester, start, end)) },
query = { attendanceDb.loadAll(semester.diaryId, semester.studentId, start.monday, end.sunday) }, query = { attendanceDb.loadAll(semester.diaryId, semester.studentId, start.monday, end.sunday) },
fetch = { fetch = {

View File

@ -10,6 +10,7 @@ import io.github.wulkanowy.utils.getRefreshKey
import io.github.wulkanowy.utils.init import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.networkBoundResource
import io.github.wulkanowy.utils.uniqueSubtract import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.sync.Mutex
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -20,9 +21,12 @@ class AttendanceSummaryRepository @Inject constructor(
private val refreshHelper: AutoRefreshHelper, private val refreshHelper: AutoRefreshHelper,
) { ) {
private val saveFetchResultMutex = Mutex()
private val cacheKey = "attendance_summary" private val cacheKey = "attendance_summary"
fun getAttendanceSummary(student: Student, semester: Semester, subjectId: Int, forceRefresh: Boolean) = networkBoundResource( fun getAttendanceSummary(student: Student, semester: Semester, subjectId: Int, forceRefresh: Boolean) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester)) }, shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester)) },
query = { attendanceDb.loadAll(semester.diaryId, semester.studentId, subjectId) }, query = { attendanceDb.loadAll(semester.diaryId, semester.studentId, subjectId) },
fetch = { fetch = {

View File

@ -12,6 +12,7 @@ import io.github.wulkanowy.utils.monday
import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.networkBoundResource
import io.github.wulkanowy.utils.sunday import io.github.wulkanowy.utils.sunday
import io.github.wulkanowy.utils.uniqueSubtract import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.sync.Mutex
import java.time.LocalDate import java.time.LocalDate
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -23,9 +24,12 @@ class CompletedLessonsRepository @Inject constructor(
private val refreshHelper: AutoRefreshHelper, private val refreshHelper: AutoRefreshHelper,
) { ) {
private val saveFetchResultMutex = Mutex()
private val cacheKey = "completed" private val cacheKey = "completed"
fun getCompletedLessons(student: Student, semester: Semester, start: LocalDate, end: LocalDate, forceRefresh: Boolean) = networkBoundResource( fun getCompletedLessons(student: Student, semester: Semester, start: LocalDate, end: LocalDate, forceRefresh: Boolean) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester, start, end)) }, shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester, start, end)) },
query = { completedLessonsDb.loadAll(semester.studentId, semester.diaryId, start.monday, end.sunday) }, query = { completedLessonsDb.loadAll(semester.studentId, semester.diaryId, start.monday, end.sunday) },
fetch = { fetch = {

View File

@ -10,6 +10,7 @@ import io.github.wulkanowy.utils.getRefreshKey
import io.github.wulkanowy.utils.init import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.networkBoundResource
import io.github.wulkanowy.utils.uniqueSubtract import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.sync.Mutex
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -20,9 +21,12 @@ class ConferenceRepository @Inject constructor(
private val refreshHelper: AutoRefreshHelper, private val refreshHelper: AutoRefreshHelper,
) { ) {
private val saveFetchResultMutex = Mutex()
private val cacheKey = "conference" private val cacheKey = "conference"
fun getConferences(student: Student, semester: Semester, forceRefresh: Boolean) = networkBoundResource( fun getConferences(student: Student, semester: Semester, forceRefresh: Boolean) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester)) }, shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester)) },
query = { conferenceDb.loadAll(semester.diaryId, student.studentId) }, query = { conferenceDb.loadAll(semester.diaryId, student.studentId) },
fetch = { fetch = {

View File

@ -12,6 +12,7 @@ import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.networkBoundResource
import io.github.wulkanowy.utils.startExamsDay import io.github.wulkanowy.utils.startExamsDay
import io.github.wulkanowy.utils.uniqueSubtract import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.sync.Mutex
import java.time.LocalDate import java.time.LocalDate
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -23,9 +24,12 @@ class ExamRepository @Inject constructor(
private val refreshHelper: AutoRefreshHelper, private val refreshHelper: AutoRefreshHelper,
) { ) {
private val saveFetchResultMutex = Mutex()
private val cacheKey = "exam" private val cacheKey = "exam"
fun getExams(student: Student, semester: Semester, start: LocalDate, end: LocalDate, forceRefresh: Boolean) = networkBoundResource( fun getExams(student: Student, semester: Semester, start: LocalDate, end: LocalDate, forceRefresh: Boolean) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester, start, end)) }, shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester, start, end)) },
query = { examDb.loadAll(semester.diaryId, semester.studentId, start.startExamsDay, start.endExamsDay) }, query = { examDb.loadAll(semester.diaryId, semester.studentId, start.startExamsDay, start.endExamsDay) },
fetch = { fetch = {

View File

@ -16,6 +16,7 @@ import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.sync.Mutex
import java.time.LocalDateTime import java.time.LocalDateTime
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -28,14 +29,20 @@ class GradeRepository @Inject constructor(
private val refreshHelper: AutoRefreshHelper, private val refreshHelper: AutoRefreshHelper,
) { ) {
private val saveFetchResultMutex = Mutex()
private val cacheKey = "grade" private val cacheKey = "grade"
fun getGrades(student: Student, semester: Semester, forceRefresh: Boolean, notify: Boolean = false) = networkBoundResource( fun getGrades(student: Student, semester: Semester, forceRefresh: Boolean, notify: Boolean = false) = networkBoundResource(
shouldFetch = { (details, summaries) -> details.isEmpty() || summaries.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester)) }, mutex = saveFetchResultMutex,
shouldFetch = { (details, summaries) ->
val isShouldBeRefreshed = refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester))
details.isEmpty() || summaries.isEmpty() || forceRefresh || isShouldBeRefreshed
},
query = { query = {
gradeDb.loadAll(semester.semesterId, semester.studentId).combine(gradeSummaryDb.loadAll(semester.semesterId, semester.studentId)) { details, summaries -> val detailsFlow = gradeDb.loadAll(semester.semesterId, semester.studentId)
details to summaries val summaryFlow = gradeSummaryDb.loadAll(semester.semesterId, semester.studentId)
} detailsFlow.combine(summaryFlow) { details, summaries -> details to summaries }
}, },
fetch = { fetch = {
val (details, summary) = sdk.init(student) val (details, summary) = sdk.init(student)
@ -92,19 +99,27 @@ class GradeRepository @Inject constructor(
} }
fun getUnreadGrades(semester: Semester): Flow<List<Grade>> { fun getUnreadGrades(semester: Semester): Flow<List<Grade>> {
return gradeDb.loadAll(semester.semesterId, semester.studentId).map { it.filter { grade -> !grade.isRead } } return gradeDb.loadAll(semester.semesterId, semester.studentId).map {
it.filter { grade -> !grade.isRead }
}
} }
fun getNotNotifiedGrades(semester: Semester): Flow<List<Grade>> { fun getNotNotifiedGrades(semester: Semester): Flow<List<Grade>> {
return gradeDb.loadAll(semester.semesterId, semester.studentId).map { it.filter { grade -> !grade.isNotified } } return gradeDb.loadAll(semester.semesterId, semester.studentId).map {
it.filter { grade -> !grade.isNotified }
}
} }
fun getNotNotifiedPredictedGrades(semester: Semester): Flow<List<GradeSummary>> { fun getNotNotifiedPredictedGrades(semester: Semester): Flow<List<GradeSummary>> {
return gradeSummaryDb.loadAll(semester.semesterId, semester.studentId).map { it.filter { gradeSummary -> !gradeSummary.isPredictedGradeNotified } } return gradeSummaryDb.loadAll(semester.semesterId, semester.studentId).map {
it.filter { gradeSummary -> !gradeSummary.isPredictedGradeNotified }
}
} }
fun getNotNotifiedFinalGrades(semester: Semester): Flow<List<GradeSummary>> { fun getNotNotifiedFinalGrades(semester: Semester): Flow<List<GradeSummary>> {
return gradeSummaryDb.loadAll(semester.semesterId, semester.studentId).map { it.filter { gradeSummary -> !gradeSummary.isFinalGradeNotified } } return gradeSummaryDb.loadAll(semester.semesterId, semester.studentId).map {
it.filter { gradeSummary -> !gradeSummary.isFinalGradeNotified }
}
} }
suspend fun updateGrade(grade: Grade) { suspend fun updateGrade(grade: Grade) {

View File

@ -17,6 +17,7 @@ import io.github.wulkanowy.utils.getRefreshKey
import io.github.wulkanowy.utils.init import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.networkBoundResource
import io.github.wulkanowy.utils.uniqueSubtract import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.sync.Mutex
import java.util.Locale import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -30,11 +31,16 @@ class GradeStatisticsRepository @Inject constructor(
private val refreshHelper: AutoRefreshHelper, private val refreshHelper: AutoRefreshHelper,
) { ) {
private val partialMutex = Mutex()
private val semesterMutex = Mutex()
private val pointsMutex = Mutex()
private val partialCacheKey = "grade_stats_partial" private val partialCacheKey = "grade_stats_partial"
private val semesterCacheKey = "grade_stats_semester" private val semesterCacheKey = "grade_stats_semester"
private val pointsCacheKey = "grade_stats_points" private val pointsCacheKey = "grade_stats_points"
fun getGradesPartialStatistics(student: Student, semester: Semester, subjectName: String, forceRefresh: Boolean) = networkBoundResource( fun getGradesPartialStatistics(student: Student, semester: Semester, subjectName: String, forceRefresh: Boolean) = networkBoundResource(
mutex = partialMutex,
shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(partialCacheKey, semester)) }, shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(partialCacheKey, semester)) },
query = { gradePartialStatisticsDb.loadAll(semester.semesterId, semester.studentId) }, query = { gradePartialStatisticsDb.loadAll(semester.semesterId, semester.studentId) },
fetch = { fetch = {
@ -71,6 +77,7 @@ class GradeStatisticsRepository @Inject constructor(
) )
fun getGradesSemesterStatistics(student: Student, semester: Semester, subjectName: String, forceRefresh: Boolean) = networkBoundResource( fun getGradesSemesterStatistics(student: Student, semester: Semester, subjectName: String, forceRefresh: Boolean) = networkBoundResource(
mutex = semesterMutex,
shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(semesterCacheKey, semester)) }, shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(semesterCacheKey, semester)) },
query = { gradeSemesterStatisticsDb.loadAll(semester.semesterId, semester.studentId) }, query = { gradeSemesterStatisticsDb.loadAll(semester.semesterId, semester.studentId) },
fetch = { fetch = {
@ -112,6 +119,7 @@ class GradeStatisticsRepository @Inject constructor(
) )
fun getGradesPointsStatistics(student: Student, semester: Semester, subjectName: String, forceRefresh: Boolean) = networkBoundResource( fun getGradesPointsStatistics(student: Student, semester: Semester, subjectName: String, forceRefresh: Boolean) = networkBoundResource(
mutex = pointsMutex,
shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(pointsCacheKey, semester)) }, shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(pointsCacheKey, semester)) },
query = { gradePointsStatisticsDb.loadAll(semester.semesterId, semester.studentId) }, query = { gradePointsStatisticsDb.loadAll(semester.semesterId, semester.studentId) },
fetch = { fetch = {

View File

@ -13,6 +13,7 @@ import io.github.wulkanowy.utils.monday
import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.networkBoundResource
import io.github.wulkanowy.utils.sunday import io.github.wulkanowy.utils.sunday
import io.github.wulkanowy.utils.uniqueSubtract import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.sync.Mutex
import java.time.LocalDate import java.time.LocalDate
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -24,9 +25,12 @@ class HomeworkRepository @Inject constructor(
private val refreshHelper: AutoRefreshHelper, private val refreshHelper: AutoRefreshHelper,
) { ) {
private val saveFetchResultMutex = Mutex()
private val cacheKey = "homework" private val cacheKey = "homework"
fun getHomework(student: Student, semester: Semester, start: LocalDate, end: LocalDate, forceRefresh: Boolean) = networkBoundResource( fun getHomework(student: Student, semester: Semester, start: LocalDate, end: LocalDate, forceRefresh: Boolean) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester, start, end)) }, shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester, start, end)) },
query = { homeworkDb.loadAll(semester.semesterId, semester.studentId, start.monday, end.sunday) }, query = { homeworkDb.loadAll(semester.semesterId, semester.studentId, start.monday, end.sunday) },
fetch = { fetch = {

View File

@ -9,6 +9,8 @@ import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.networkBoundResource
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.sync.Mutex
import java.time.LocalDate
import java.time.LocalDate.now import java.time.LocalDate.now
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -19,7 +21,10 @@ class LuckyNumberRepository @Inject constructor(
private val sdk: Sdk private val sdk: Sdk
) { ) {
private val saveFetchResultMutex = Mutex()
fun getLuckyNumber(student: Student, forceRefresh: Boolean, notify: Boolean = false) = networkBoundResource( fun getLuckyNumber(student: Student, forceRefresh: Boolean, notify: Boolean = false) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = { it == null || forceRefresh }, shouldFetch = { it == null || forceRefresh },
query = { luckyNumberDb.load(student.studentId, now()) }, query = { luckyNumberDb.load(student.studentId, now()) },
fetch = { sdk.init(student).getLuckyNumber(student.schoolShortName)?.mapToEntity(student) }, fetch = { sdk.init(student).getLuckyNumber(student.schoolShortName)?.mapToEntity(student) },
@ -33,6 +38,9 @@ class LuckyNumberRepository @Inject constructor(
} }
) )
fun getLuckyNumberHistory(student: Student, start: LocalDate, end: LocalDate) =
luckyNumberDb.getAll(student.studentId, start, end)
suspend fun getNotNotifiedLuckyNumber(student: Student) = luckyNumberDb.load(student.studentId, now()).map { suspend fun getNotNotifiedLuckyNumber(student: Student) = luckyNumberDb.load(student.studentId, now()).map {
if (it?.isNotified == false) it else null if (it?.isNotified == false) it else null
}.first() }.first()

View File

@ -20,6 +20,7 @@ import io.github.wulkanowy.utils.networkBoundResource
import io.github.wulkanowy.utils.uniqueSubtract import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.sync.Mutex
import timber.log.Timber import timber.log.Timber
import java.time.LocalDateTime.now import java.time.LocalDateTime.now
import javax.inject.Inject import javax.inject.Inject
@ -33,10 +34,13 @@ class MessageRepository @Inject constructor(
private val refreshHelper: AutoRefreshHelper, private val refreshHelper: AutoRefreshHelper,
) { ) {
private val saveFetchResultMutex = Mutex()
private val cacheKey = "message" private val cacheKey = "message"
@Suppress("UNUSED_PARAMETER") @Suppress("UNUSED_PARAMETER")
fun getMessages(student: Student, semester: Semester, folder: MessageFolder, forceRefresh: Boolean, notify: Boolean = false) = networkBoundResource( fun getMessages(student: Student, semester: Semester, folder: MessageFolder, forceRefresh: Boolean, notify: Boolean = false) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, student, folder)) }, shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, student, folder)) },
query = { messagesDb.loadAll(student.id.toInt(), folder.id) }, query = { messagesDb.loadAll(student.id.toInt(), folder.id) },
fetch = { sdk.init(student).getMessages(Folder.valueOf(folder.name), now().minusMonths(3), now()).mapToEntities(student) }, fetch = { sdk.init(student).getMessages(Folder.valueOf(folder.name), now().minusMonths(3), now()).mapToEntities(student) },

View File

@ -13,6 +13,7 @@ import io.github.wulkanowy.utils.getRefreshKey
import io.github.wulkanowy.utils.init import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.networkBoundResource
import io.github.wulkanowy.utils.uniqueSubtract import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.sync.Mutex
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -23,9 +24,12 @@ class MobileDeviceRepository @Inject constructor(
private val refreshHelper: AutoRefreshHelper, private val refreshHelper: AutoRefreshHelper,
) { ) {
private val saveFetchResultMutex = Mutex()
private val cacheKey = "devices" private val cacheKey = "devices"
fun getDevices(student: Student, semester: Semester, forceRefresh: Boolean) = networkBoundResource( fun getDevices(student: Student, semester: Semester, forceRefresh: Boolean) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, student)) }, shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, student)) },
query = { mobileDb.loadAll(student.userLoginId.takeIf { it != 0 } ?: student.studentId) }, query = { mobileDb.loadAll(student.userLoginId.takeIf { it != 0 } ?: student.studentId) },
fetch = { fetch = {

View File

@ -13,6 +13,7 @@ import io.github.wulkanowy.utils.networkBoundResource
import io.github.wulkanowy.utils.uniqueSubtract import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.sync.Mutex
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -23,9 +24,12 @@ class NoteRepository @Inject constructor(
private val refreshHelper: AutoRefreshHelper, private val refreshHelper: AutoRefreshHelper,
) { ) {
private val saveFetchResultMutex = Mutex()
private val cacheKey = "note" private val cacheKey = "note"
fun getNotes(student: Student, semester: Semester, forceRefresh: Boolean, notify: Boolean = false) = networkBoundResource( fun getNotes(student: Student, semester: Semester, forceRefresh: Boolean, notify: Boolean = false) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester)) }, shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester)) },
query = { noteDb.loadAll(student.studentId) }, query = { noteDb.loadAll(student.studentId) },
fetch = { fetch = {

View File

@ -18,26 +18,43 @@ class PreferencesRepository @Inject constructor(
get() = getString(R.string.pref_key_start_menu, R.string.pref_default_startup).toInt() get() = getString(R.string.pref_key_start_menu, R.string.pref_default_startup).toInt()
val isShowPresent: Boolean val isShowPresent: Boolean
get() = getBoolean(R.string.pref_key_attendance_present, R.bool.pref_default_attendance_present) get() = getBoolean(
R.string.pref_key_attendance_present,
R.bool.pref_default_attendance_present
)
val gradeAverageMode: GradeAverageMode val gradeAverageMode: GradeAverageMode
get() = GradeAverageMode.getByValue(getString(R.string.pref_key_grade_average_mode, R.string.pref_default_grade_average_mode)) get() = GradeAverageMode.getByValue(
getString(
R.string.pref_key_grade_average_mode,
R.string.pref_default_grade_average_mode
)
)
val gradeAverageForceCalc: Boolean val gradeAverageForceCalc: Boolean
get() = getBoolean(R.string.pref_key_grade_average_force_calc, R.bool.pref_default_grade_average_force_calc) get() = getBoolean(
R.string.pref_key_grade_average_force_calc,
R.bool.pref_default_grade_average_force_calc
)
val isGradeExpandable: Boolean val isGradeExpandable: Boolean
get() = !getBoolean(R.string.pref_key_expand_grade, R.bool.pref_default_expand_grade) get() = !getBoolean(R.string.pref_key_expand_grade, R.bool.pref_default_expand_grade)
val showAllSubjectsOnStatisticsList: Boolean val showAllSubjectsOnStatisticsList: Boolean
get() = getBoolean(R.string.pref_key_grade_statistics_list, R.bool.pref_default_grade_statistics_list) get() = getBoolean(
R.string.pref_key_grade_statistics_list,
R.bool.pref_default_grade_statistics_list
)
val appThemeKey = context.getString(R.string.pref_key_app_theme) val appThemeKey = context.getString(R.string.pref_key_app_theme)
val appTheme: String val appTheme: String
get() = getString(appThemeKey, R.string.pref_default_app_theme) get() = getString(appThemeKey, R.string.pref_default_app_theme)
val gradeColorTheme: String val gradeColorTheme: String
get() = getString(R.string.pref_key_grade_color_scheme, R.string.pref_default_grade_color_scheme) get() = getString(
R.string.pref_key_grade_color_scheme,
R.string.pref_default_grade_color_scheme
)
val appLanguageKey = context.getString(R.string.pref_key_app_language) val appLanguageKey = context.getString(R.string.pref_key_app_language)
val appLanguage val appLanguage
@ -55,50 +72,86 @@ class PreferencesRepository @Inject constructor(
val isServicesOnlyWifi: Boolean val isServicesOnlyWifi: Boolean
get() = getBoolean(servicesOnlyWifiKey, R.bool.pref_default_services_wifi_only) get() = getBoolean(servicesOnlyWifiKey, R.bool.pref_default_services_wifi_only)
val notificationsEnableKey = context.getString(R.string.pref_key_notifications_enable)
val isNotificationsEnable: Boolean val isNotificationsEnable: Boolean
get() = getBoolean(R.string.pref_key_notifications_enable, R.bool.pref_default_notifications_enable) get() = getBoolean(notificationsEnableKey, R.bool.pref_default_notifications_enable)
val isUpcomingLessonsNotificationsEnableKey = context.getString(R.string.pref_key_notifications_upcoming_lessons_enable) val isUpcomingLessonsNotificationsEnableKey =
context.getString(R.string.pref_key_notifications_upcoming_lessons_enable)
val isUpcomingLessonsNotificationsEnable: Boolean val isUpcomingLessonsNotificationsEnable: Boolean
get() = getBoolean(isUpcomingLessonsNotificationsEnableKey, R.bool.pref_default_notification_upcoming_lessons_enable) get() = getBoolean(
isUpcomingLessonsNotificationsEnableKey,
R.bool.pref_default_notification_upcoming_lessons_enable
)
val isDebugNotificationEnableKey = context.getString(R.string.pref_key_notification_debug) val isDebugNotificationEnableKey = context.getString(R.string.pref_key_notification_debug)
val isDebugNotificationEnable: Boolean val isDebugNotificationEnable: Boolean
get() = getBoolean(isDebugNotificationEnableKey, R.bool.pref_default_notification_debug) get() = getBoolean(isDebugNotificationEnableKey, R.bool.pref_default_notification_debug)
val gradePlusModifier: Double val gradePlusModifier: Double
get() = getString(R.string.pref_key_grade_modifier_plus, R.string.pref_default_grade_modifier_plus).toDouble() get() = getString(
R.string.pref_key_grade_modifier_plus,
R.string.pref_default_grade_modifier_plus
).toDouble()
val gradeMinusModifier: Double val gradeMinusModifier: Double
get() = getString(R.string.pref_key_grade_modifier_minus, R.string.pref_default_grade_modifier_minus).toDouble() get() = getString(
R.string.pref_key_grade_modifier_minus,
R.string.pref_default_grade_modifier_minus
).toDouble()
val fillMessageContent: Boolean val fillMessageContent: Boolean
get() = getBoolean(R.string.pref_key_fill_message_content, R.bool.pref_default_fill_message_content) get() = getBoolean(
R.string.pref_key_fill_message_content,
R.bool.pref_default_fill_message_content
)
val showGroupsInPlan: Boolean val showGroupsInPlan: Boolean
get() = getBoolean(R.string.pref_key_timetable_show_groups, R.bool.pref_default_timetable_show_groups) get() = getBoolean(
R.string.pref_key_timetable_show_groups,
R.bool.pref_default_timetable_show_groups
)
val showWholeClassPlan: String val showWholeClassPlan: String
get() = getString(R.string.pref_key_timetable_show_whole_class, R.string.pref_default_timetable_show_whole_class) get() = getString(
R.string.pref_key_timetable_show_whole_class,
R.string.pref_default_timetable_show_whole_class
)
val gradeSortingMode: GradeSortingMode val gradeSortingMode: GradeSortingMode
get() = GradeSortingMode.getByValue(getString(R.string.pref_key_grade_sorting_mode, R.string.pref_default_grade_sorting_mode)) get() = GradeSortingMode.getByValue(
getString(
R.string.pref_key_grade_sorting_mode,
R.string.pref_default_grade_sorting_mode
)
)
val showTimetableTimers: Boolean val showTimetableTimers: Boolean
get() = getBoolean(R.string.pref_key_timetable_show_timers, R.bool.pref_default_timetable_show_timers) get() = getBoolean(
R.string.pref_key_timetable_show_timers,
R.bool.pref_default_timetable_show_timers
)
var isHomeworkFullscreen: Boolean var isHomeworkFullscreen: Boolean
get() = getBoolean(R.string.pref_key_homework_fullscreen, R.bool.pref_default_homework_fullscreen) get() = getBoolean(
R.string.pref_key_homework_fullscreen,
R.bool.pref_default_homework_fullscreen
)
set(value) = sharedPref.edit().putBoolean("homework_fullscreen", value).apply() set(value) = sharedPref.edit().putBoolean("homework_fullscreen", value).apply()
val showSubjectsWithoutGrades: Boolean val showSubjectsWithoutGrades: Boolean
get() = getBoolean(R.string.pref_key_subjects_without_grades, R.bool.pref_default_subjects_without_grades) get() = getBoolean(
R.string.pref_key_subjects_without_grades,
R.bool.pref_default_subjects_without_grades
)
private fun getString(id: Int, default: Int) = getString(context.getString(id), default) private fun getString(id: Int, default: Int) = getString(context.getString(id), default)
private fun getString(id: String, default: Int) = sharedPref.getString(id, context.getString(default)) ?: context.getString(default) private fun getString(id: String, default: Int) =
sharedPref.getString(id, context.getString(default)) ?: context.getString(default)
private fun getBoolean(id: Int, default: Int) = getBoolean(context.getString(id), default) private fun getBoolean(id: Int, default: Int) = getBoolean(context.getString(id), default)
private fun getBoolean(id: String, default: Int) = sharedPref.getBoolean(id, context.resources.getBoolean(default)) private fun getBoolean(id: String, default: Int) =
sharedPref.getBoolean(id, context.resources.getBoolean(default))
} }

View File

@ -7,6 +7,7 @@ import io.github.wulkanowy.data.mappers.mapToEntity
import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.init import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.networkBoundResource
import kotlinx.coroutines.sync.Mutex
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -16,8 +17,11 @@ class SchoolRepository @Inject constructor(
private val sdk: Sdk private val sdk: Sdk
) { ) {
private val saveFetchResultMutex = Mutex()
fun getSchoolInfo(student: Student, semester: Semester, forceRefresh: Boolean) = fun getSchoolInfo(student: Student, semester: Semester, forceRefresh: Boolean) =
networkBoundResource( networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = { it == null || forceRefresh }, shouldFetch = { it == null || forceRefresh },
query = { schoolDb.load(semester.studentId, semester.classId) }, query = { schoolDb.load(semester.studentId, semester.classId) },
fetch = { fetch = {

View File

@ -7,6 +7,7 @@ import io.github.wulkanowy.data.mappers.mapToEntity
import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.init import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.networkBoundResource
import kotlinx.coroutines.sync.Mutex
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -16,8 +17,11 @@ class StudentInfoRepository @Inject constructor(
private val sdk: Sdk private val sdk: Sdk
) { ) {
private val saveFetchResultMutex = Mutex()
fun getStudentInfo(student: Student, semester: Semester, forceRefresh: Boolean) = fun getStudentInfo(student: Student, semester: Semester, forceRefresh: Boolean) =
networkBoundResource( networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = { it == null || forceRefresh }, shouldFetch = { it == null || forceRefresh },
query = { studentInfoDao.loadStudentInfo(student.studentId) }, query = { studentInfoDao.loadStudentInfo(student.studentId) },
fetch = { fetch = {

View File

@ -5,11 +5,12 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.data.db.dao.SemesterDao import io.github.wulkanowy.data.db.dao.SemesterDao
import io.github.wulkanowy.data.db.dao.StudentDao import io.github.wulkanowy.data.db.dao.StudentDao
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentNick import io.github.wulkanowy.data.db.entities.StudentNickAndAvatar
import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.data.exceptions.NoCurrentStudentException import io.github.wulkanowy.data.exceptions.NoCurrentStudentException
import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.DispatchersProvider import io.github.wulkanowy.utils.DispatchersProvider
import io.github.wulkanowy.utils.security.decrypt import io.github.wulkanowy.utils.security.decrypt
import io.github.wulkanowy.utils.security.encrypt import io.github.wulkanowy.utils.security.encrypt
@ -23,7 +24,8 @@ class StudentRepository @Inject constructor(
private val dispatchers: DispatchersProvider, private val dispatchers: DispatchersProvider,
private val studentDb: StudentDao, private val studentDb: StudentDao,
private val semesterDb: SemesterDao, private val semesterDb: SemesterDao,
private val sdk: Sdk private val sdk: Sdk,
private val appInfo: AppInfo
) { ) {
suspend fun isStudentSaved() = getSavedStudents(false).isNotEmpty() suspend fun isStudentSaved() = getSavedStudents(false).isNotEmpty()
@ -35,7 +37,8 @@ class StudentRepository @Inject constructor(
symbol: String, symbol: String,
token: String token: String
): List<StudentWithSemesters> = ): List<StudentWithSemesters> =
sdk.getStudentsFromMobileApi(token, pin, symbol, "").mapToEntities() sdk.getStudentsFromMobileApi(token, pin, symbol, "")
.mapToEntities(colors = appInfo.defaultColorsForAvatar)
suspend fun getStudentsScrapper( suspend fun getStudentsScrapper(
email: String, email: String,
@ -44,7 +47,7 @@ class StudentRepository @Inject constructor(
symbol: String symbol: String
): List<StudentWithSemesters> = ): List<StudentWithSemesters> =
sdk.getStudentsFromScrapper(email, password, scrapperBaseUrl, symbol) sdk.getStudentsFromScrapper(email, password, scrapperBaseUrl, symbol)
.mapToEntities(password) .mapToEntities(password, appInfo.defaultColorsForAvatar)
suspend fun getStudentsHybrid( suspend fun getStudentsHybrid(
email: String, email: String,
@ -52,47 +55,59 @@ class StudentRepository @Inject constructor(
scrapperBaseUrl: String, scrapperBaseUrl: String,
symbol: String symbol: String
): List<StudentWithSemesters> = ): List<StudentWithSemesters> =
sdk.getStudentsHybrid(email, password, scrapperBaseUrl, "", symbol).mapToEntities(password) sdk.getStudentsHybrid(email, password, scrapperBaseUrl, "", symbol)
.mapToEntities(password, appInfo.defaultColorsForAvatar)
suspend fun getSavedStudents(decryptPass: Boolean = true) = suspend fun getSavedStudents(decryptPass: Boolean = true) =
withContext(dispatchers.backgroundThread) { studentDb.loadStudentsWithSemesters()
studentDb.loadStudentsWithSemesters().map { .map {
it.apply { it.apply {
if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API) { if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API) {
student.password = decrypt(student.password) student.password = withContext(dispatchers.backgroundThread) {
decrypt(student.password)
} }
} }
} }
} }
suspend fun getStudentById(id: Int) = withContext(dispatchers.backgroundThread) { suspend fun getStudentById(id: Long, decryptPass: Boolean = true): Student {
studentDb.loadById(id)?.apply { val student = studentDb.loadById(id) ?: throw NoCurrentStudentException()
if (Sdk.Mode.valueOf(loginMode) != Sdk.Mode.API) {
password = decrypt(password)
}
}
} ?: throw NoCurrentStudentException()
suspend fun getCurrentStudent(decryptPass: Boolean = true) = if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API) {
withContext(dispatchers.backgroundThread) { student.password = withContext(dispatchers.backgroundThread) {
studentDb.loadCurrent()?.apply { decrypt(student.password)
if (decryptPass && Sdk.Mode.valueOf(loginMode) != Sdk.Mode.API) {
password = decrypt(password)
} }
} }
} ?: throw NoCurrentStudentException() return student
}
suspend fun getCurrentStudent(decryptPass: Boolean = true): Student {
val student = studentDb.loadCurrent() ?: throw NoCurrentStudentException()
if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API) {
student.password = withContext(dispatchers.backgroundThread) {
decrypt(student.password)
}
}
return student
}
suspend fun saveStudents(studentsWithSemesters: List<StudentWithSemesters>): List<Long> { suspend fun saveStudents(studentsWithSemesters: List<StudentWithSemesters>): List<Long> {
semesterDb.insertSemesters(studentsWithSemesters.flatMap { it.semesters }) val semesters = studentsWithSemesters.flatMap { it.semesters }
val students = studentsWithSemesters.map { it.student }
return withContext(dispatchers.backgroundThread) { .map {
studentDb.insertAll(studentsWithSemesters.map { it.student }.map { it.apply {
if (Sdk.Mode.valueOf(it.loginMode) != Sdk.Mode.API) { if (Sdk.Mode.valueOf(it.loginMode) != Sdk.Mode.API) {
it.copy(password = encrypt(it.password, context)) password = withContext(dispatchers.backgroundThread) {
} else it encrypt(password, context)
})
} }
} }
}
}
semesterDb.insertSemesters(semesters)
return studentDb.insertAll(students)
}
suspend fun switchStudent(studentWithSemesters: StudentWithSemesters) { suspend fun switchStudent(studentWithSemesters: StudentWithSemesters) {
with(studentDb) { with(studentDb) {
@ -103,5 +118,6 @@ class StudentRepository @Inject constructor(
suspend fun logoutStudent(student: Student) = studentDb.delete(student) suspend fun logoutStudent(student: Student) = studentDb.delete(student)
suspend fun updateStudentNick(studentNick: StudentNick) = studentDb.update(studentNick) suspend fun updateStudentNickAndAvatar(studentNickAndAvatar: StudentNickAndAvatar) =
studentDb.update(studentNickAndAvatar)
} }

View File

@ -8,6 +8,7 @@ import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.init import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.networkBoundResource
import io.github.wulkanowy.utils.uniqueSubtract import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.sync.Mutex
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -17,7 +18,10 @@ class SubjectRepository @Inject constructor(
private val sdk: Sdk private val sdk: Sdk
) { ) {
private val saveFetchResultMutex = Mutex()
fun getSubjects(student: Student, semester: Semester, forceRefresh: Boolean = false) = networkBoundResource( fun getSubjects(student: Student, semester: Semester, forceRefresh: Boolean = false) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = { it.isEmpty() || forceRefresh }, shouldFetch = { it.isEmpty() || forceRefresh },
query = { subjectDao.loadAll(semester.diaryId, semester.studentId) }, query = { subjectDao.loadAll(semester.diaryId, semester.studentId) },
fetch = { fetch = {

View File

@ -8,6 +8,7 @@ import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.init import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.networkBoundResource
import io.github.wulkanowy.utils.uniqueSubtract import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.sync.Mutex
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -17,7 +18,10 @@ class TeacherRepository @Inject constructor(
private val sdk: Sdk private val sdk: Sdk
) { ) {
private val saveFetchResultMutex = Mutex()
fun getTeachers(student: Student, semester: Semester, forceRefresh: Boolean) = networkBoundResource( fun getTeachers(student: Student, semester: Semester, forceRefresh: Boolean) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = { it.isEmpty() || forceRefresh }, shouldFetch = { it.isEmpty() || forceRefresh },
query = { teacherDb.loadAll(semester.studentId, semester.classId) }, query = { teacherDb.loadAll(semester.studentId, semester.classId) },
fetch = { fetch = {

View File

@ -18,6 +18,7 @@ import io.github.wulkanowy.utils.sunday
import io.github.wulkanowy.utils.uniqueSubtract import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.sync.Mutex
import java.time.LocalDate import java.time.LocalDate
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -31,9 +32,12 @@ class TimetableRepository @Inject constructor(
private val refreshHelper: AutoRefreshHelper, private val refreshHelper: AutoRefreshHelper,
) { ) {
private val saveFetchResultMutex = Mutex()
private val cacheKey = "timetable" private val cacheKey = "timetable"
fun getTimetable(student: Student, semester: Semester, start: LocalDate, end: LocalDate, forceRefresh: Boolean, refreshAdditional: Boolean = false) = networkBoundResource( fun getTimetable(student: Student, semester: Semester, start: LocalDate, end: LocalDate, forceRefresh: Boolean, refreshAdditional: Boolean = false) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = { (timetable, additional) -> timetable.isEmpty() || (additional.isEmpty() && refreshAdditional) || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester, start, end)) }, shouldFetch = { (timetable, additional) -> timetable.isEmpty() || (additional.isEmpty() && refreshAdditional) || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester, start, end)) },
query = { query = {
timetableDb.loadAll(semester.diaryId, semester.studentId, start.monday, end.sunday) timetableDb.loadAll(semester.diaryId, semester.studentId, start.monday, end.sunday)

View File

@ -37,6 +37,7 @@ abstract class BaseActivity<T : BasePresenter<out BaseView>, VB : ViewBinding> :
abstract var presenter: T abstract var presenter: T
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
inject()
themeManager.applyActivityTheme(this) themeManager.applyActivityTheme(this)
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleLogger, true) supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleLogger, true)
@ -44,7 +45,9 @@ abstract class BaseActivity<T : BasePresenter<out BaseView>, VB : ViewBinding> :
if (SDK_INT >= LOLLIPOP) { if (SDK_INT >= LOLLIPOP) {
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
setTaskDescription(ActivityManager.TaskDescription(null, null, getThemeAttrColor(R.attr.colorSurface))) setTaskDescription(
ActivityManager.TaskDescription(null, null, getThemeAttrColor(R.attr.colorSurface))
)
} }
} }
@ -84,4 +87,9 @@ abstract class BaseActivity<T : BasePresenter<out BaseView>, VB : ViewBinding> :
invalidateOptionsMenu() invalidateOptionsMenu()
presenter.onDetachView() presenter.onDetachView()
} }
//https://github.com/google/dagger/releases/tag/dagger-2.33
protected open fun inject() {
throw UnsupportedOperationException()
}
} }

View File

@ -4,6 +4,7 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentPagerAdapter import androidx.fragment.app.FragmentPagerAdapter
//TODO Use ViewPager2
class BaseFragmentPagerAdapter(private val fragmentManager: FragmentManager) : class BaseFragmentPagerAdapter(private val fragmentManager: FragmentManager) :
FragmentPagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { FragmentPagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {

View File

@ -42,12 +42,10 @@ class ErrorDialog : BaseDialogFragment<DialogErrorBinding>() {
companion object { companion object {
private const val ARGUMENT_KEY = "Data" private const val ARGUMENT_KEY = "Data"
fun newInstance(error: Throwable): ErrorDialog { fun newInstance(error: Throwable) = ErrorDialog().apply {
return ErrorDialog().apply {
arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, error) } arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, error) }
} }
} }
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -57,12 +55,14 @@ class ErrorDialog : BaseDialogFragment<DialogErrorBinding>() {
} }
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateView(
return DialogErrorBinding.inflate(inflater).apply { binding = this }.root inflater: LayoutInflater,
} container: ViewGroup?,
savedInstanceState: Bundle?
) = DialogErrorBinding.inflate(inflater).apply { binding = this }.root
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onViewCreated(view, savedInstanceState)
val stringWriter = StringWriter().apply { val stringWriter = StringWriter().apply {
error.printStackTrace(PrintWriter(this)) error.printStackTrace(PrintWriter(this))

View File

@ -8,6 +8,9 @@ import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_NO
import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_YES import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_YES
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.ui.modules.login.LoginActivity
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.message.send.SendMessageActivity
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -17,7 +20,13 @@ class ThemeManager @Inject constructor(private val preferencesRepository: Prefer
fun applyActivityTheme(activity: AppCompatActivity) { fun applyActivityTheme(activity: AppCompatActivity) {
if (isThemeApplicable(activity)) { if (isThemeApplicable(activity)) {
applyDefaultTheme() applyDefaultTheme()
if (preferencesRepository.appTheme == "black") activity.setTheme(R.style.WulkanowyTheme_Black) if (preferencesRepository.appTheme == "black") {
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)
}
}
} }
} }
@ -33,8 +42,13 @@ class ThemeManager @Inject constructor(private val preferencesRepository: Prefer
} }
private fun isThemeApplicable(activity: AppCompatActivity): Boolean { private fun isThemeApplicable(activity: AppCompatActivity): Boolean {
return activity.packageManager.getPackageInfo(activity.packageName, GET_ACTIVITIES) return activity.packageManager
.activities.singleOrNull { it.name == activity::class.java.canonicalName }?.theme .getPackageInfo(activity.packageName, GET_ACTIVITIES)
.let { it == R.style.WulkanowyTheme_Black || it == R.style.WulkanowyTheme_NoActionBar } .activities.singleOrNull { it.name == activity::class.java.canonicalName }
?.theme.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
}
} }
} }

View File

@ -4,9 +4,9 @@ import android.os.Bundle
import android.view.View import android.view.View
import android.view.View.GONE import android.view.View.GONE
import android.view.View.VISIBLE import android.view.View.VISIBLE
import androidx.appcompat.app.AlertDialog
import androidx.core.text.parseAsHtml import androidx.core.text.parseAsHtml
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.mikepenz.aboutlibraries.Libs import com.mikepenz.aboutlibraries.Libs
import com.mikepenz.aboutlibraries.entity.Library import com.mikepenz.aboutlibraries.entity.Library
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
@ -26,14 +26,9 @@ class LicenseFragment : BaseFragment<FragmentLicenseBinding>(R.layout.fragment_l
@Inject @Inject
lateinit var licenseAdapter: LicenseAdapter lateinit var licenseAdapter: LicenseAdapter
private val libs by lazy { Libs(requireContext()) }
override val titleStringId get() = R.string.license_title override val titleStringId get() = R.string.license_title
override val appLibraries: ArrayList<Library>? override val appLibraries by lazy { Libs(requireContext()).libraries }
get() = context?.let {
libs.prepareLibraries(it, emptyArray(), emptyArray(), autoDetect = true, checkCachedDetection = true, sort = true)
}
companion object { companion object {
fun newInstance() = LicenseFragment() fun newInstance() = LicenseFragment()
@ -63,7 +58,7 @@ class LicenseFragment : BaseFragment<FragmentLicenseBinding>(R.layout.fragment_l
override fun openLicense(licenseHtml: String) { override fun openLicense(licenseHtml: String) {
context?.let { context?.let {
AlertDialog.Builder(it).apply { MaterialAlertDialogBuilder(it).apply {
setTitle(R.string.license_dialog_title) setTitle(R.string.license_dialog_title)
setMessage(licenseHtml.parseAsHtml()) setMessage(licenseHtml.parseAsHtml())
setPositiveButton(android.R.string.ok) { _, _ -> } setPositiveButton(android.R.string.ok) { _, _ -> }

View File

@ -5,7 +5,7 @@ import io.github.wulkanowy.ui.base.BaseView
interface LicenseView : BaseView { interface LicenseView : BaseView {
val appLibraries: ArrayList<Library>? val appLibraries: List<Library>
fun initView() fun initView()

View File

@ -1,16 +1,17 @@
package io.github.wulkanowy.ui.modules.account package io.github.wulkanowy.ui.modules.account
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.graphics.PorterDuff
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View.GONE import android.view.View.GONE
import android.view.View.VISIBLE import android.view.View.VISIBLE
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.databinding.HeaderAccountBinding import io.github.wulkanowy.databinding.HeaderAccountBinding
import io.github.wulkanowy.databinding.ItemAccountBinding import io.github.wulkanowy.databinding.ItemAccountBinding
import io.github.wulkanowy.utils.createNameInitialsDrawable
import io.github.wulkanowy.utils.getThemeAttrColor import io.github.wulkanowy.utils.getThemeAttrColor
import io.github.wulkanowy.utils.nickOrName import io.github.wulkanowy.utils.nickOrName
import javax.inject.Inject import javax.inject.Inject
@ -72,9 +73,13 @@ class AccountAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView.V
binding: ItemAccountBinding, binding: ItemAccountBinding,
studentWithSemesters: StudentWithSemesters studentWithSemesters: StudentWithSemesters
) { ) {
val context = binding.root.context
val student = studentWithSemesters.student val student = studentWithSemesters.student
val semesters = studentWithSemesters.semesters val semesters = studentWithSemesters.semesters
val diary = semesters.maxByOrNull { it.semesterId } val diary = semesters.maxByOrNull { it.semesterId }
val avatar = context.createNameInitialsDrawable(student.nickOrName, student.avatarColor)
val checkBackgroundColor =
context.getThemeAttrColor(if (isAccountQuickDialogMode) R.attr.colorBackgroundFloating else R.attr.colorSurface)
val isDuplicatedStudent = items.filter { val isDuplicatedStudent = items.filter {
if (it.value !is StudentWithSemesters) return@filter false if (it.value !is StudentWithSemesters) return@filter false
val studentToCompare = it.value.student val studentToCompare = it.value.student
@ -87,15 +92,17 @@ class AccountAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView.V
with(binding) { with(binding) {
accountItemName.text = "${student.nickOrName} ${diary?.diaryName.orEmpty()}" accountItemName.text = "${student.nickOrName} ${diary?.diaryName.orEmpty()}"
accountItemSchool.text = studentWithSemesters.student.schoolName accountItemSchool.text = studentWithSemesters.student.schoolName
accountItemAccountType.setText(if (student.isParent) R.string.account_type_parent else R.string.account_type_student) accountItemImage.setImageDrawable(avatar)
accountItemAccountType.visibility = if (isDuplicatedStudent) VISIBLE else GONE
with(accountItemImage) { with(accountItemAccountType) {
val colorImage = setText(if (student.isParent) R.string.account_type_parent else R.string.account_type_student)
if (student.isCurrent) context.getThemeAttrColor(R.attr.colorPrimary) isVisible = isDuplicatedStudent
else context.getThemeAttrColor(R.attr.colorOnSurface, 153) }
setColorFilter(colorImage, PorterDuff.Mode.SRC_IN) with(accountItemCheck) {
isVisible = student.isCurrent
borderColor = checkBackgroundColor
circleColor = checkBackgroundColor
} }
root.setOnClickListener { onClickListener(studentWithSemesters) } root.setOnClickListener { onClickListener(studentWithSemesters) }

View File

@ -34,25 +34,20 @@ class AccountFragment : BaseFragment<FragmentAccountBinding>(R.layout.fragment_a
override val titleStringId = R.string.account_title override val titleStringId = R.string.account_title
override var subtitleString = ""
override val isViewEmpty get() = accountAdapter.items.isEmpty()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setHasOptionsMenu(true) setHasOptionsMenu(true)
} }
@Suppress("UNCHECKED_CAST")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
binding = FragmentAccountBinding.bind(view) binding = FragmentAccountBinding.bind(view)
presenter.onAttachView(this) presenter.onAttachView(this)
} }
override fun initView() { override fun initView() {
binding.accountErrorRetry.setOnClickListener { presenter.onRetry() }
binding.accountErrorDetails.setOnClickListener { presenter.onDetailsClick() }
binding.accountRecycler.apply { binding.accountRecycler.apply {
layoutManager = LinearLayoutManager(context) layoutManager = LinearLayoutManager(context)
adapter = accountAdapter adapter = accountAdapter
@ -60,9 +55,7 @@ class AccountFragment : BaseFragment<FragmentAccountBinding>(R.layout.fragment_a
accountAdapter.onClickListener = presenter::onItemSelected accountAdapter.onClickListener = presenter::onItemSelected
with(binding) { binding.accountAdd.setOnClickListener { presenter.onAddSelected() }
accountAdd.setOnClickListener { presenter.onAddSelected() }
}
} }
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
@ -84,28 +77,7 @@ class AccountFragment : BaseFragment<FragmentAccountBinding>(R.layout.fragment_a
override fun openAccountDetailsView(studentWithSemesters: StudentWithSemesters) { override fun openAccountDetailsView(studentWithSemesters: StudentWithSemesters) {
(activity as? MainActivity)?.pushView( (activity as? MainActivity)?.pushView(
AccountDetailsFragment.newInstance( AccountDetailsFragment.newInstance(studentWithSemesters)
studentWithSemesters
) )
)
}
override fun showErrorView(show: Boolean) {
binding.accountError.visibility = if (show) View.VISIBLE else View.GONE
}
override fun setErrorDetails(message: String) {
binding.accountErrorMessage.text = message
}
override fun showProgress(show: Boolean) {
binding.accountProgress.visibility = if (show) View.VISIBLE else View.GONE
}
override fun showContent(show: Boolean) {
with(binding) {
accountRecycler.visibility = if (show) View.VISIBLE else View.GONE
accountAdd.visibility = if (show) View.VISIBLE else View.GONE
}
} }
} }

View File

@ -5,7 +5,6 @@ import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.utils.afterLoading
import io.github.wulkanowy.utils.flowWithResource import io.github.wulkanowy.utils.flowWithResource
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import timber.log.Timber import timber.log.Timber
@ -16,28 +15,13 @@ class AccountPresenter @Inject constructor(
studentRepository: StudentRepository, studentRepository: StudentRepository,
) : BasePresenter<AccountView>(errorHandler, studentRepository) { ) : BasePresenter<AccountView>(errorHandler, studentRepository) {
private lateinit var lastError: Throwable
override fun onAttachView(view: AccountView) { override fun onAttachView(view: AccountView) {
super.onAttachView(view) super.onAttachView(view)
view.initView() view.initView()
Timber.i("Account view was initialized") Timber.i("Account view was initialized")
errorHandler.showErrorMessage = ::showErrorViewOnError
loadData() loadData()
} }
fun onRetry() {
view?.run {
showErrorView(false)
showProgress(true)
}
loadData()
}
fun onDetailsClick() {
view?.showErrorDetailsDialog(lastError)
}
fun onAddSelected() { fun onAddSelected() {
Timber.i("Select add account") Timber.i("Select add account")
view?.openLoginView() view?.openLoginView()
@ -47,6 +31,24 @@ class AccountPresenter @Inject constructor(
view?.openAccountDetailsView(studentWithSemesters) view?.openAccountDetailsView(studentWithSemesters)
} }
private fun loadData() {
flowWithResource { studentRepository.getSavedStudents(false) }
.onEach {
when (it.status) {
Status.LOADING -> Timber.i("Loading account data started")
Status.SUCCESS -> {
Timber.i("Loading account result: Success")
view?.updateData(createAccountItems(it.data!!))
}
Status.ERROR -> {
Timber.i("Loading account result: An exception occurred")
errorHandler.dispatch(it.error!!)
}
}
}
.launch("load")
}
private fun createAccountItems(items: List<StudentWithSemesters>): List<AccountItem<*>> { private fun createAccountItems(items: List<StudentWithSemesters>): List<AccountItem<*>> {
return items.groupBy { return items.groupBy {
Account("${it.student.userName} (${it.student.email})", it.student.isParent) Account("${it.student.userName} (${it.student.email})", it.student.isParent)
@ -60,45 +62,4 @@ class AccountPresenter @Inject constructor(
} }
.flatten() .flatten()
} }
private fun loadData() {
flowWithResource { studentRepository.getSavedStudents(false) }
.onEach {
when (it.status) {
Status.LOADING -> {
Timber.i("Loading account data started")
view?.run {
showProgress(true)
showContent(false)
}
}
Status.SUCCESS -> {
Timber.i("Loading account result: Success")
view?.updateData(createAccountItems(it.data!!))
view?.run {
showContent(true)
showErrorView(false)
}
}
Status.ERROR -> {
Timber.i("Loading account result: An exception occurred")
errorHandler.dispatch(it.error!!)
}
}
}
.afterLoading { view?.showProgress(false) }
.launch()
}
private fun showErrorViewOnError(message: String, error: Throwable) {
view?.run {
if (isViewEmpty) {
lastError = error
setErrorDetails(message)
showErrorView(true)
showContent(false)
showProgress(false)
} else showError(message, error)
}
}
} }

View File

@ -5,8 +5,6 @@ import io.github.wulkanowy.ui.base.BaseView
interface AccountView : BaseView { interface AccountView : BaseView {
val isViewEmpty: Boolean
fun initView() fun initView()
fun updateData(data: List<AccountItem<*>>) fun updateData(data: List<AccountItem<*>>)
@ -14,13 +12,4 @@ interface AccountView : BaseView {
fun openLoginView() fun openLoginView()
fun openAccountDetailsView(studentWithSemesters: StudentWithSemesters) fun openAccountDetailsView(studentWithSemesters: StudentWithSemesters)
fun showErrorView(show: Boolean)
fun setErrorDetails(message: String)
fun showProgress(show: Boolean)
fun showContent(show: Boolean)
} }

View File

@ -7,6 +7,7 @@ import android.view.MenuItem
import android.view.View import android.view.View
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.view.get import androidx.core.view.get
import androidx.core.view.isVisible
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
@ -18,6 +19,7 @@ import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.ui.modules.studentinfo.StudentInfoFragment import io.github.wulkanowy.ui.modules.studentinfo.StudentInfoFragment
import io.github.wulkanowy.ui.modules.studentinfo.StudentInfoView import io.github.wulkanowy.ui.modules.studentinfo.StudentInfoView
import io.github.wulkanowy.utils.createNameInitialsDrawable
import io.github.wulkanowy.utils.nickOrName import io.github.wulkanowy.utils.nickOrName
import javax.inject.Inject import javax.inject.Inject
@ -31,8 +33,6 @@ class AccountDetailsFragment :
override val titleStringId = R.string.account_details_title override val titleStringId = R.string.account_details_title
override var subtitleString = ""
companion object { companion object {
private const val ARGUMENT_KEY = "Data" private const val ARGUMENT_KEY = "Data"
@ -88,8 +88,15 @@ class AccountDetailsFragment :
override fun showAccountData(student: Student) { override fun showAccountData(student: Student) {
with(binding) { with(binding) {
accountDetailsCheck.isVisible = student.isCurrent
accountDetailsName.text = student.nickOrName accountDetailsName.text = student.nickOrName
accountDetailsSchool.text = student.schoolName accountDetailsSchool.text = student.schoolName
accountDetailsAvatar.setImageDrawable(
requireContext().createNameInitialsDrawable(
student.nickOrName,
student.avatarColor
)
)
} }
} }

View File

@ -21,7 +21,7 @@ class AccountDetailsPresenter @Inject constructor(
private val syncManager: SyncManager private val syncManager: SyncManager
) : BasePresenter<AccountDetailsView>(errorHandler, studentRepository) { ) : BasePresenter<AccountDetailsView>(errorHandler, studentRepository) {
private lateinit var studentWithSemesters: StudentWithSemesters private var studentWithSemesters: StudentWithSemesters? = null
private lateinit var lastError: Throwable private lateinit var lastError: Throwable
@ -69,10 +69,10 @@ class AccountDetailsPresenter @Inject constructor(
} }
Status.SUCCESS -> { Status.SUCCESS -> {
Timber.i("Loading account details view result: Success") Timber.i("Loading account details view result: Success")
studentWithSemesters = it.data!! studentWithSemesters = it.data
view?.run { view?.run {
showAccountData(studentWithSemesters.student) showAccountData(studentWithSemesters!!.student)
enableSelectStudentButton(!studentWithSemesters.student.isCurrent) enableSelectStudentButton(!studentWithSemesters!!.student.isCurrent)
showContent(true) showContent(true)
showErrorView(false) showErrorView(false)
} }
@ -88,17 +88,23 @@ class AccountDetailsPresenter @Inject constructor(
} }
fun onAccountEditSelected() { fun onAccountEditSelected() {
view?.showAccountEditDetailsDialog(studentWithSemesters.student) studentWithSemesters?.let {
view?.showAccountEditDetailsDialog(it.student)
}
} }
fun onStudentInfoSelected(infoType: StudentInfoView.Type) { fun onStudentInfoSelected(infoType: StudentInfoView.Type) {
view?.openStudentInfoView(infoType, studentWithSemesters) studentWithSemesters?.let {
view?.openStudentInfoView(infoType, it)
}
} }
fun onStudentSelect() { fun onStudentSelect() {
Timber.i("Select student ${studentWithSemesters.student.id}") if (studentWithSemesters == null) return
flowWithResource { studentRepository.switchStudent(studentWithSemesters) } Timber.i("Select student ${studentWithSemesters!!.student.id}")
flowWithResource { studentRepository.switchStudent(studentWithSemesters!!) }
.onEach { .onEach {
when (it.status) { when (it.status) {
Status.LOADING -> Timber.i("Attempt to change a student") Status.LOADING -> Timber.i("Attempt to change a student")
@ -122,8 +128,10 @@ class AccountDetailsPresenter @Inject constructor(
} }
fun onLogoutConfirm() { fun onLogoutConfirm() {
if (studentWithSemesters == null) return
flowWithResource { flowWithResource {
val studentToLogout = studentWithSemesters.student val studentToLogout = studentWithSemesters!!.student
studentRepository.logoutStudent(studentToLogout) studentRepository.logoutStudent(studentToLogout)
val students = studentRepository.getSavedStudents(false) val students = studentRepository.getSavedStudents(false)
@ -143,7 +151,7 @@ class AccountDetailsPresenter @Inject constructor(
syncManager.stopSyncWorker() syncManager.stopSyncWorker()
openClearLoginView() openClearLoginView()
} }
studentWithSemesters.student.isCurrent -> { studentWithSemesters!!.student.isCurrent -> {
Timber.i("Logout result: Logout student and switch to another") Timber.i("Logout result: Logout student and switch to another")
recreateMainView() recreateMainView()
} }

View File

@ -0,0 +1,90 @@
package io.github.wulkanowy.ui.modules.account.accountedit
import android.annotation.SuppressLint
import android.content.res.ColorStateList
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.graphics.drawable.GradientDrawable
import android.graphics.drawable.RippleDrawable
import android.graphics.drawable.StateListDrawable
import android.os.Build
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import io.github.wulkanowy.databinding.ItemAccountEditColorBinding
import javax.inject.Inject
class AccountEditColorAdapter @Inject constructor() :
RecyclerView.Adapter<AccountEditColorAdapter.ViewHolder>() {
var items = listOf<Int>()
var selectedColor = 0
override fun getItemCount() = items.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
ItemAccountEditColorBinding.inflate(LayoutInflater.from(parent.context), parent, false)
)
@SuppressLint("RestrictedApi", "NewApi")
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = items[position]
with(holder.binding) {
accountEditItemColor.setImageDrawable(GradientDrawable().apply {
shape = GradientDrawable.OVAL
setColor(item)
})
accountEditItemColorContainer.foreground = item.createForegroundDrawable()
accountEditCheck.isVisible = selectedColor == item
root.setOnClickListener {
val oldSelectedPosition = items.indexOf(selectedColor)
selectedColor = item
notifyItemChanged(oldSelectedPosition)
notifyItemChanged(position)
}
}
}
private fun Int.createForegroundDrawable(): Drawable =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val mask = GradientDrawable().apply {
shape = GradientDrawable.OVAL
setColor(Color.BLACK)
}
RippleDrawable(ColorStateList.valueOf(this.rippleColor), null, mask)
} else {
val foreground = StateListDrawable().apply {
alpha = 80
setEnterFadeDuration(250)
setExitFadeDuration(250)
}
val mask = GradientDrawable().apply {
shape = GradientDrawable.OVAL
setColor(this@createForegroundDrawable.rippleColor)
}
foreground.apply {
addState(intArrayOf(android.R.attr.state_pressed), mask)
addState(intArrayOf(), ColorDrawable(Color.TRANSPARENT))
}
}
private inline val Int.rippleColor: Int
get() {
val hsv = FloatArray(3)
Color.colorToHSV(this, hsv)
hsv[2] = hsv[2] * 0.5f
return Color.HSVToColor(hsv)
}
class ViewHolder(val binding: ItemAccountEditColorBinding) :
RecyclerView.ViewHolder(binding.root)
}

View File

@ -4,6 +4,7 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.GridLayoutManager
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.databinding.DialogAccountEditBinding import io.github.wulkanowy.databinding.DialogAccountEditBinding
@ -16,6 +17,9 @@ class AccountEditDialog : BaseDialogFragment<DialogAccountEditBinding>(), Accoun
@Inject @Inject
lateinit var presenter: AccountEditPresenter lateinit var presenter: AccountEditPresenter
@Inject
lateinit var accountEditColorAdapter: AccountEditColorAdapter
companion object { companion object {
private const val ARGUMENT_KEY = "student_with_semesters" private const val ARGUMENT_KEY = "student_with_semesters"
@ -48,8 +52,30 @@ class AccountEditDialog : BaseDialogFragment<DialogAccountEditBinding>(), Accoun
with(binding) { with(binding) {
accountEditDetailsCancel.setOnClickListener { dismiss() } accountEditDetailsCancel.setOnClickListener { dismiss() }
accountEditDetailsSave.setOnClickListener { accountEditDetailsSave.setOnClickListener {
presenter.changeStudentNick(binding.accountEditDetailsNickText.text.toString()) presenter.changeStudentNickAndAvatar(
binding.accountEditDetailsNickText.text.toString(),
accountEditColorAdapter.selectedColor
)
} }
with(binding.accountEditColors) {
layoutManager = GridLayoutManager(context, 4)
adapter = accountEditColorAdapter
}
}
}
override fun updateSelectedColorData(color: Int) {
with(accountEditColorAdapter) {
selectedColor = color
notifyDataSetChanged()
}
}
override fun updateColorsData(colors: List<Int>) {
with(accountEditColorAdapter) {
items = colors
notifyDataSetChanged()
} }
} }

View File

@ -2,10 +2,11 @@ package io.github.wulkanowy.ui.modules.account.accountedit
import io.github.wulkanowy.data.Status import io.github.wulkanowy.data.Status
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentNick import io.github.wulkanowy.data.db.entities.StudentNickAndAvatar
import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.afterLoading import io.github.wulkanowy.utils.afterLoading
import io.github.wulkanowy.utils.flowWithResource import io.github.wulkanowy.utils.flowWithResource
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
@ -13,12 +14,15 @@ import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
class AccountEditPresenter @Inject constructor( class AccountEditPresenter @Inject constructor(
private val appInfo: AppInfo,
errorHandler: ErrorHandler, errorHandler: ErrorHandler,
studentRepository: StudentRepository studentRepository: StudentRepository
) : BasePresenter<AccountEditView>(errorHandler, studentRepository) { ) : BasePresenter<AccountEditView>(errorHandler, studentRepository) {
lateinit var student: Student lateinit var student: Student
private val colors = appInfo.defaultColorsForAvatar.map { it.toInt() }
fun onAttachView(view: AccountEditView, student: Student) { fun onAttachView(view: AccountEditView, student: Student) {
super.onAttachView(view) super.onAttachView(view)
this.student = student this.student = student
@ -28,27 +32,49 @@ class AccountEditPresenter @Inject constructor(
showCurrentNick(student.nick.trim()) showCurrentNick(student.nick.trim())
} }
Timber.i("Account edit dialog view was initialized") Timber.i("Account edit dialog view was initialized")
loadData()
view.updateColorsData(colors)
} }
fun changeStudentNick(nick: String) { private fun loadData() {
flowWithResource {
studentRepository.getStudentById(student.id, false).avatarColor
}.onEach { resource ->
when (resource.status) {
Status.LOADING -> Timber.i("Attempt to load student")
Status.SUCCESS -> {
view?.updateSelectedColorData(resource.data?.toInt()!!)
Timber.i("Attempt to load student: Success")
}
Status.ERROR -> {
Timber.i("Attempt to load student: An exception occurred")
errorHandler.dispatch(resource.error!!)
}
}
}.launch("load_data")
}
fun changeStudentNickAndAvatar(nick: String, avatarColor: Int) {
flowWithResource { flowWithResource {
val studentNick = val studentNick =
StudentNick(nick = nick.trim()).apply { id = student.id } StudentNickAndAvatar(nick = nick.trim(), avatarColor = avatarColor.toLong())
studentRepository.updateStudentNick(studentNick) .apply { id = student.id }
studentRepository.updateStudentNickAndAvatar(studentNick)
}.onEach { }.onEach {
when (it.status) { when (it.status) {
Status.LOADING -> Timber.i("Attempt to change a student nick") Status.LOADING -> Timber.i("Attempt to change a student nick and avatar")
Status.SUCCESS -> { Status.SUCCESS -> {
Timber.i("Change a student nick result: Success") Timber.i("Change a student nick and avatar result: Success")
view?.recreateMainView() view?.recreateMainView()
} }
Status.ERROR -> { Status.ERROR -> {
Timber.i("Change a student result: An exception occurred") Timber.i("Change a student nick and avatar result: An exception occurred")
errorHandler.dispatch(it.error!!) errorHandler.dispatch(it.error!!)
} }
} }
} }
.afterLoading { view?.popView() } .afterLoading { view?.popView() }
.launch() .launch("update_student")
} }
} }

View File

@ -11,4 +11,8 @@ interface AccountEditView : BaseView {
fun recreateMainView() fun recreateMainView()
fun showCurrentNick(nick: String) fun showCurrentNick(nick: String)
fun updateSelectedColorData(color: Int)
fun updateColorsData(colors: List<Int>)
} }

View File

@ -6,6 +6,7 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.databinding.DialogAccountQuickBinding import io.github.wulkanowy.databinding.DialogAccountQuickBinding
import io.github.wulkanowy.ui.base.BaseDialogFragment import io.github.wulkanowy.ui.base.BaseDialogFragment
import io.github.wulkanowy.ui.modules.account.AccountAdapter import io.github.wulkanowy.ui.modules.account.AccountAdapter
@ -24,7 +25,15 @@ class AccountQuickDialog : BaseDialogFragment<DialogAccountQuickBinding>(), Acco
lateinit var presenter: AccountQuickPresenter lateinit var presenter: AccountQuickPresenter
companion object { companion object {
fun newInstance() = AccountQuickDialog()
private const val STUDENTS_ARGUMENT_KEY = "students"
fun newInstance(studentsWithSemesters: List<StudentWithSemesters>) =
AccountQuickDialog().apply {
arguments = Bundle().apply {
putSerializable(STUDENTS_ARGUMENT_KEY, studentsWithSemesters.toTypedArray())
}
}
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -38,8 +47,12 @@ class AccountQuickDialog : BaseDialogFragment<DialogAccountQuickBinding>(), Acco
savedInstanceState: Bundle? savedInstanceState: Bundle?
) = DialogAccountQuickBinding.inflate(inflater).apply { binding = this }.root ) = DialogAccountQuickBinding.inflate(inflater).apply { binding = this }.root
@Suppress("UNCHECKED_CAST")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
presenter.onAttachView(this) val studentsWithSemesters =
(requireArguments()[STUDENTS_ARGUMENT_KEY] as Array<StudentWithSemesters>).toList()
presenter.onAttachView(this, studentsWithSemesters)
} }
override fun initView() { override fun initView() {

View File

@ -17,11 +17,15 @@ class AccountQuickPresenter @Inject constructor(
studentRepository: StudentRepository studentRepository: StudentRepository
) : BasePresenter<AccountQuickView>(errorHandler, studentRepository) { ) : BasePresenter<AccountQuickView>(errorHandler, studentRepository) {
override fun onAttachView(view: AccountQuickView) { private lateinit var studentsWithSemesters: List<StudentWithSemesters>
fun onAttachView(view: AccountQuickView, studentsWithSemesters: List<StudentWithSemesters>) {
super.onAttachView(view) super.onAttachView(view)
this.studentsWithSemesters = studentsWithSemesters
view.initView() view.initView()
Timber.i("Account quick dialog view was initialized") Timber.i("Account quick dialog view was initialized")
loadData() view.updateData(createAccountItems(studentsWithSemesters))
} }
fun onManagerSelected() { fun onManagerSelected() {
@ -57,22 +61,6 @@ class AccountQuickPresenter @Inject constructor(
.launch("switch") .launch("switch")
} }
private fun loadData() {
flowWithResource { studentRepository.getSavedStudents(false) }.onEach {
when (it.status) {
Status.LOADING -> Timber.i("Loading account data started")
Status.SUCCESS -> {
Timber.i("Loading account result: Success")
view?.updateData(createAccountItems(it.data!!))
}
Status.ERROR -> {
Timber.i("Loading account result: An exception occurred")
errorHandler.dispatch(it.error!!)
}
}
}.launch()
}
private fun createAccountItems(items: List<StudentWithSemesters>) = items.map { private fun createAccountItems(items: List<StudentWithSemesters>) = items.map {
AccountItem(it, AccountItem.ViewType.ITEM) AccountItem(it, AccountItem.ViewType.ITEM)
} }

View File

@ -18,14 +18,13 @@ class AttendanceDialog : DialogFragment() {
private lateinit var attendance: Attendance private lateinit var attendance: Attendance
companion object { companion object {
private const val ARGUMENT_KEY = "Item" private const val ARGUMENT_KEY = "Item"
fun newInstance(exam: Attendance): AttendanceDialog { fun newInstance(exam: Attendance) = AttendanceDialog().apply {
return AttendanceDialog().apply {
arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, exam) } arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, exam) }
} }
} }
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -35,12 +34,14 @@ class AttendanceDialog : DialogFragment() {
} }
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(
return DialogAttendanceBinding.inflate(inflater).apply { binding = this }.root inflater: LayoutInflater,
} container: ViewGroup?,
savedInstanceState: Bundle?
) = DialogAttendanceBinding.inflate(inflater).apply { binding = this }.root
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onViewCreated(view, savedInstanceState)
with(binding) { with(binding) {
attendanceDialogSubject.text = attendance.subject attendanceDialogSubject.text = attendance.subject

View File

@ -17,14 +17,13 @@ class ExamDialog : DialogFragment() {
private lateinit var exam: Exam private lateinit var exam: Exam
companion object { companion object {
private const val ARGUMENT_KEY = "Item" private const val ARGUMENT_KEY = "Item"
fun newInstance(exam: Exam): ExamDialog { fun newInstance(exam: Exam) = ExamDialog().apply {
return ExamDialog().apply {
arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, exam) } arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, exam) }
} }
} }
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -34,12 +33,14 @@ class ExamDialog : DialogFragment() {
} }
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(
return DialogExamBinding.inflate(inflater).apply { binding = this }.root inflater: LayoutInflater,
} container: ViewGroup?,
savedInstanceState: Bundle?
) = DialogExamBinding.inflate(inflater).apply { binding = this }.root
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onViewCreated(view, savedInstanceState)
with(binding) { with(binding) {
examDialogSubjectValue.text = exam.subject examDialogSubjectValue.text = exam.subject

View File

@ -86,7 +86,11 @@ class GradeAverageProvider @Inject constructor(
return@combine firstSemesterGradeSubject return@combine firstSemesterGradeSubject
} }
val isAnyAverage = secondSemesterGradeSubject.data.orEmpty().any { it.average != .0 } val isAnyVulcanAverageInFirstSemester =
firstSemesterGradeSubject.data.orEmpty().any { it.average != .0 }
val isAnyVulcanAverageInSecondSemester =
secondSemesterGradeSubject.data.orEmpty().any { it.average != .0 }
val updatedData = secondSemesterGradeSubject.data?.map { secondSemesterSubject -> val updatedData = secondSemesterGradeSubject.data?.map { secondSemesterSubject ->
val firstSemesterSubject = firstSemesterGradeSubject.data.orEmpty() val firstSemesterSubject = firstSemesterGradeSubject.data.orEmpty()
.singleOrNull { it.subject == secondSemesterSubject.subject } .singleOrNull { it.subject == secondSemesterSubject.subject }
@ -94,7 +98,7 @@ class GradeAverageProvider @Inject constructor(
val updatedAverage = if (averageMode == ALL_YEAR) { val updatedAverage = if (averageMode == ALL_YEAR) {
calculateAllYearAverage( calculateAllYearAverage(
student = student, student = student,
isAnyAverage = isAnyAverage, isAnyVulcanAverage = isAnyVulcanAverageInFirstSemester || isAnyVulcanAverageInSecondSemester,
gradeAverageForceCalc = gradeAverageForceCalc, gradeAverageForceCalc = gradeAverageForceCalc,
secondSemesterSubject = secondSemesterSubject, secondSemesterSubject = secondSemesterSubject,
firstSemesterSubject = firstSemesterSubject firstSemesterSubject = firstSemesterSubject
@ -102,7 +106,7 @@ class GradeAverageProvider @Inject constructor(
} else { } else {
calculateBothSemestersAverage( calculateBothSemestersAverage(
student = student, student = student,
isAnyAverage = isAnyAverage, isAnyVulcanAverage = isAnyVulcanAverageInFirstSemester || isAnyVulcanAverageInSecondSemester,
gradeAverageForceCalc = gradeAverageForceCalc, gradeAverageForceCalc = gradeAverageForceCalc,
secondSemesterSubject = secondSemesterSubject, secondSemesterSubject = secondSemesterSubject,
firstSemesterSubject = firstSemesterSubject firstSemesterSubject = firstSemesterSubject
@ -116,11 +120,11 @@ class GradeAverageProvider @Inject constructor(
private fun calculateAllYearAverage( private fun calculateAllYearAverage(
student: Student, student: Student,
isAnyAverage: Boolean, isAnyVulcanAverage: Boolean,
gradeAverageForceCalc: Boolean, gradeAverageForceCalc: Boolean,
secondSemesterSubject: GradeSubject, secondSemesterSubject: GradeSubject,
firstSemesterSubject: GradeSubject? firstSemesterSubject: GradeSubject?
) = if (!isAnyAverage || gradeAverageForceCalc) { ) = if (!isAnyVulcanAverage || gradeAverageForceCalc) {
val updatedSecondSemesterGrades = val updatedSecondSemesterGrades =
secondSemesterSubject.grades.updateModifiers(student) secondSemesterSubject.grades.updateModifiers(student)
val updatedFirstSemesterGrades = val updatedFirstSemesterGrades =
@ -133,20 +137,23 @@ class GradeAverageProvider @Inject constructor(
private fun calculateBothSemestersAverage( private fun calculateBothSemestersAverage(
student: Student, student: Student,
isAnyAverage: Boolean, isAnyVulcanAverage: Boolean,
gradeAverageForceCalc: Boolean, gradeAverageForceCalc: Boolean,
secondSemesterSubject: GradeSubject, secondSemesterSubject: GradeSubject,
firstSemesterSubject: GradeSubject? firstSemesterSubject: GradeSubject?
) = if (!isAnyAverage || gradeAverageForceCalc) { ): Double {
val divider = if (secondSemesterSubject.grades.any { it.weightValue > .0 }) 2 else 1
return if (!isAnyVulcanAverage || gradeAverageForceCalc) {
val secondSemesterAverage = val secondSemesterAverage =
secondSemesterSubject.grades.updateModifiers(student).calcAverage() secondSemesterSubject.grades.updateModifiers(student).calcAverage()
val firstSemesterAverage = firstSemesterSubject?.grades?.updateModifiers(student) val firstSemesterAverage = firstSemesterSubject?.grades?.updateModifiers(student)
?.calcAverage() ?: secondSemesterAverage ?.calcAverage() ?: secondSemesterAverage
val divider = if (secondSemesterSubject.grades.any { it.weightValue > .0 }) 2 else 1
(secondSemesterAverage + firstSemesterAverage) / divider (secondSemesterAverage + firstSemesterAverage) / divider
} else { } else {
(secondSemesterSubject.average + (firstSemesterSubject?.average ?: secondSemesterSubject.average)) / 2 (secondSemesterSubject.average + (firstSemesterSubject?.average ?: secondSemesterSubject.average)) / divider
}
} }
private fun getGradeSubjects( private fun getGradeSubjects(

View File

@ -63,11 +63,13 @@ class GradeFragment : BaseFragment<FragmentGradeBinding>(R.layout.fragment_grade
override fun initView() { override fun initView() {
with(pagerAdapter) { with(pagerAdapter) {
containerId = binding.gradeViewPager.id containerId = binding.gradeViewPager.id
addFragmentsWithTitle(mapOf( addFragmentsWithTitle(
mapOf(
GradeDetailsFragment.newInstance() to getString(R.string.all_details), GradeDetailsFragment.newInstance() to getString(R.string.all_details),
GradeSummaryFragment.newInstance() to getString(R.string.grade_menu_summary), GradeSummaryFragment.newInstance() to getString(R.string.grade_menu_summary),
GradeStatisticsFragment.newInstance() to getString(R.string.grade_menu_statistics) GradeStatisticsFragment.newInstance() to getString(R.string.grade_menu_statistics)
)) )
)
} }
with(binding.gradeViewPager) { with(binding.gradeViewPager) {
@ -137,7 +139,10 @@ class GradeFragment : BaseFragment<FragmentGradeBinding>(R.layout.fragment_grade
override fun setCurrentSemesterName(semester: Int, schoolYear: Int) { override fun setCurrentSemesterName(semester: Int, schoolYear: Int) {
subtitleString = getString(R.string.grade_subtitle, semester, schoolYear, schoolYear + 1) subtitleString = getString(R.string.grade_subtitle, semester, schoolYear, schoolYear + 1)
(activity as MainView).setViewSubTitle(subtitleString)
if (isVisible) {
(activity as MainView?)?.setViewSubTitle(subtitleString)
}
} }
fun onChildRefresh() { fun onChildRefresh() {
@ -149,7 +154,8 @@ class GradeFragment : BaseFragment<FragmentGradeBinding>(R.layout.fragment_grade
} }
override fun notifyChildLoadData(index: Int, semesterId: Int, forceRefresh: Boolean) { override fun notifyChildLoadData(index: Int, semesterId: Int, forceRefresh: Boolean) {
(pagerAdapter.getFragmentInstance(index) as? GradeView.GradeChildView)?.onParentLoadData(semesterId, forceRefresh) (pagerAdapter.getFragmentInstance(index) as? GradeView.GradeChildView)
?.onParentLoadData(semesterId, forceRefresh)
} }
override fun notifyChildParentReselected(index: Int) { override fun notifyChildParentReselected(index: Int) {

View File

@ -24,18 +24,19 @@ class GradeDetailsDialog : DialogFragment() {
private lateinit var colorScheme: String private lateinit var colorScheme: String
companion object { companion object {
private const val ARGUMENT_KEY = "Item" private const val ARGUMENT_KEY = "Item"
private const val COLOR_SCHEME_KEY = "Scheme" private const val COLOR_SCHEME_KEY = "Scheme"
fun newInstance(grade: Grade, colorScheme: String): GradeDetailsDialog { fun newInstance(grade: Grade, colorScheme: String) =
return GradeDetailsDialog().apply { GradeDetailsDialog().apply {
arguments = Bundle().apply { arguments = Bundle().apply {
putSerializable(ARGUMENT_KEY, grade) putSerializable(ARGUMENT_KEY, grade)
putString(COLOR_SCHEME_KEY, colorScheme) putString(COLOR_SCHEME_KEY, colorScheme)
} }
} }
} }
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -46,12 +47,14 @@ class GradeDetailsDialog : DialogFragment() {
} }
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(
return DialogGradeBinding.inflate(inflater).apply { binding = this }.root inflater: LayoutInflater,
} container: ViewGroup?,
savedInstanceState: Bundle?
) = DialogGradeBinding.inflate(inflater).apply { binding = this }.root
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onViewCreated(view, savedInstanceState)
with(binding) { with(binding) {
gradeDialogSubject.text = grade.subject gradeDialogSubject.text = grade.subject

View File

@ -22,6 +22,7 @@ import io.github.wulkanowy.data.db.entities.GradePointsStatistics
import io.github.wulkanowy.data.db.entities.GradeSemesterStatistics import io.github.wulkanowy.data.db.entities.GradeSemesterStatistics
import io.github.wulkanowy.data.pojos.GradeStatisticsItem import io.github.wulkanowy.data.pojos.GradeStatisticsItem
import io.github.wulkanowy.databinding.ItemGradeStatisticsBarBinding import io.github.wulkanowy.databinding.ItemGradeStatisticsBarBinding
import io.github.wulkanowy.databinding.ItemGradeStatisticsHeaderBinding
import io.github.wulkanowy.databinding.ItemGradeStatisticsPieBinding import io.github.wulkanowy.databinding.ItemGradeStatisticsPieBinding
import io.github.wulkanowy.utils.getThemeAttrColor import io.github.wulkanowy.utils.getThemeAttrColor
import javax.inject.Inject import javax.inject.Inject
@ -29,12 +30,16 @@ import javax.inject.Inject
class GradeStatisticsAdapter @Inject constructor() : class GradeStatisticsAdapter @Inject constructor() :
RecyclerView.Adapter<RecyclerView.ViewHolder>() { RecyclerView.Adapter<RecyclerView.ViewHolder>() {
var currentDataType = GradeStatisticsItem.DataType.PARTIAL
var items = emptyList<GradeStatisticsItem>() var items = emptyList<GradeStatisticsItem>()
var theme: String = "vulcan" var theme: String = "vulcan"
var showAllSubjectsOnList: Boolean = false var showAllSubjectsOnList: Boolean = false
var onDataTypeChangeListener: () -> Unit = {}
private val vulcanGradeColors = listOf( private val vulcanGradeColors = listOf(
6 to R.color.grade_vulcan_six, 6 to R.color.grade_vulcan_six,
5 to R.color.grade_vulcan_five, 5 to R.color.grade_vulcan_five,
@ -62,37 +67,90 @@ class GradeStatisticsAdapter @Inject constructor() :
"6, 6-", "5, 5-, 5+", "4, 4-, 4+", "3, 3-, 3+", "2, 2-, 2+", "1, 1+" "6, 6-", "5, 5-, 5+", "4, 4-, 4+", "3, 3-, 3+", "2, 2-, 2+", "1, 1+"
) )
override fun getItemCount() = if (showAllSubjectsOnList) items.size else (if (items.isEmpty()) 0 else 1) override fun getItemCount() =
(if (showAllSubjectsOnList) items.size else (if (items.isEmpty()) 0 else 1)) + 1
override fun getItemViewType(position: Int) = items[position].type.id override fun getItemViewType(position: Int) =
if (position == 0) {
ViewType.HEADER.id
} else {
when (items[position - 1].type) {
GradeStatisticsItem.DataType.PARTIAL -> ViewType.PARTIAL.id
GradeStatisticsItem.DataType.POINTS -> ViewType.POINTS.id
GradeStatisticsItem.DataType.SEMESTER -> ViewType.SEMESTER.id
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context) val inflater = LayoutInflater.from(parent.context)
return when (viewType) { return when (viewType) {
ViewType.PARTIAL.id -> PartialViewHolder(ItemGradeStatisticsPieBinding.inflate(inflater, parent, false)) ViewType.PARTIAL.id -> PartialViewHolder(
ViewType.SEMESTER.id -> SemesterViewHolder(ItemGradeStatisticsPieBinding.inflate(inflater, parent, false)) ItemGradeStatisticsPieBinding.inflate(inflater, parent, false)
ViewType.POINTS.id -> PointsViewHolder(ItemGradeStatisticsBarBinding.inflate(inflater, parent, false)) )
ViewType.SEMESTER.id -> SemesterViewHolder(
ItemGradeStatisticsPieBinding.inflate(inflater, parent, false)
)
ViewType.POINTS.id -> PointsViewHolder(
ItemGradeStatisticsBarBinding.inflate(inflater, parent, false)
)
ViewType.HEADER.id -> HeaderViewHolder(
ItemGradeStatisticsHeaderBinding.inflate(inflater, parent, false)
)
else -> throw IllegalStateException() else -> throw IllegalStateException()
} }
} }
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val index = position - 1
when (holder) { when (holder) {
is PartialViewHolder -> bindPartialChart(holder.binding, items[position].partial!!) is PartialViewHolder -> bindPartialChart(holder.binding, items[index].partial!!)
is SemesterViewHolder -> bindSemesterChart(holder.binding, items[position].semester!!) is SemesterViewHolder -> bindSemesterChart(holder.binding, items[index].semester!!)
is PointsViewHolder -> bindBarChart(holder.binding, items[position].points!!) is PointsViewHolder -> bindBarChart(holder.binding, items[index].points!!)
is HeaderViewHolder -> bindHeader(holder.binding)
} }
} }
private fun bindPartialChart(binding: ItemGradeStatisticsPieBinding, partials: GradePartialStatistics) { private fun bindHeader(binding: ItemGradeStatisticsHeaderBinding) {
binding.gradeStatisticsTypeSwitch.check(
when (currentDataType) {
GradeStatisticsItem.DataType.PARTIAL -> R.id.gradeStatisticsTypePartial
GradeStatisticsItem.DataType.SEMESTER -> R.id.gradeStatisticsTypeSemester
GradeStatisticsItem.DataType.POINTS -> R.id.gradeStatisticsTypePoints
}
)
binding.gradeStatisticsTypeSwitch.setOnCheckedChangeListener { _, checkedId ->
currentDataType = when (checkedId) {
R.id.gradeStatisticsTypePartial -> GradeStatisticsItem.DataType.PARTIAL
R.id.gradeStatisticsTypeSemester -> GradeStatisticsItem.DataType.SEMESTER
R.id.gradeStatisticsTypePoints -> GradeStatisticsItem.DataType.POINTS
else -> GradeStatisticsItem.DataType.PARTIAL
}
onDataTypeChangeListener()
}
}
private fun bindPartialChart(
binding: ItemGradeStatisticsPieBinding,
partials: GradePartialStatistics
) {
bindPieChart(binding, partials.subject, partials.classAverage, partials.classAmounts) bindPieChart(binding, partials.subject, partials.classAverage, partials.classAmounts)
} }
private fun bindSemesterChart(binding: ItemGradeStatisticsPieBinding, semester: GradeSemesterStatistics) { private fun bindSemesterChart(
binding: ItemGradeStatisticsPieBinding,
semester: GradeSemesterStatistics
) {
bindPieChart(binding, semester.subject, semester.average, semester.amounts) bindPieChart(binding, semester.subject, semester.average, semester.amounts)
} }
private fun bindPieChart(binding: ItemGradeStatisticsPieBinding, subject: String, average: String, amounts: List<Int>) { private fun bindPieChart(
binding: ItemGradeStatisticsPieBinding,
subject: String,
average: String,
amounts: List<Int>
) {
with(binding.gradeStatisticsPieTitle) { with(binding.gradeStatisticsPieTitle) {
text = subject text = subject
visibility = if (items.size == 1 || !showAllSubjectsOnList) GONE else VISIBLE visibility = if (items.size == 1 || !showAllSubjectsOnList) GONE else VISIBLE
@ -114,7 +172,8 @@ class GradeStatisticsAdapter @Inject constructor() :
valueTextSize = 12f valueTextSize = 12f
sliceSpace = 1f sliceSpace = 1f
valueTextColor = Color.WHITE valueTextColor = Color.WHITE
val grades = amounts.mapIndexed { grade, amount -> (grade + 1) to amount }.filterNot { it.second == 0 } val grades = amounts.mapIndexed { grade, amount -> (grade + 1) to amount }
.filterNot { it.second == 0 }
setColors(grades.reversed().map { (grade, _) -> setColors(grades.reversed().map { (grade, _) ->
gradeColors.single { color -> color.first == grade }.second gradeColors.single { color -> color.first == grade }.second
}.toIntArray(), binding.root.context) }.toIntArray(), binding.root.context)
@ -126,7 +185,11 @@ class GradeStatisticsAdapter @Inject constructor() :
data = PieData(dataset).apply { data = PieData(dataset).apply {
setValueFormatter(object : ValueFormatter() { setValueFormatter(object : ValueFormatter() {
override fun getPieLabel(value: Float, pieEntry: PieEntry): String { override fun getPieLabel(value: Float, pieEntry: PieEntry): String {
return resources.getQuantityString(R.plurals.grade_number_item, value.toInt(), value.toInt()) return resources.getQuantityString(
R.plurals.grade_number_item,
value.toInt(),
value.toInt()
)
} }
}) })
} }
@ -143,11 +206,14 @@ class GradeStatisticsAdapter @Inject constructor() :
val numberOfGradesString = amounts.fold(0) { acc, it -> acc + it } val numberOfGradesString = amounts.fold(0) { acc, it -> acc + it }
.let { resources.getQuantityString(R.plurals.grade_number_item, it, it) } .let { resources.getQuantityString(R.plurals.grade_number_item, it, it) }
val averageString = binding.root.context.getString(R.string.grade_statistics_average, average) val averageString =
binding.root.context.getString(R.string.grade_statistics_average, average)
minAngleForSlices = 25f minAngleForSlices = 25f
description.isEnabled = false description.isEnabled = false
centerText = numberOfGradesString + ("\n\n" + averageString).takeIf { average.isNotBlank() }.orEmpty() centerText =
numberOfGradesString + ("\n\n" + averageString).takeIf { average.isNotBlank() }
.orEmpty()
setHoleColor(context.getThemeAttrColor(android.R.attr.windowBackground)) setHoleColor(context.getThemeAttrColor(android.R.attr.windowBackground))
setCenterTextColor(context.getThemeAttrColor(android.R.attr.textColorPrimary)) setCenterTextColor(context.getThemeAttrColor(android.R.attr.textColorPrimary))
@ -155,16 +221,21 @@ class GradeStatisticsAdapter @Inject constructor() :
} }
} }
private fun bindBarChart(binding: ItemGradeStatisticsBarBinding, points: GradePointsStatistics) { private fun bindBarChart(
binding: ItemGradeStatisticsBarBinding,
points: GradePointsStatistics
) {
with(binding.gradeStatisticsBarTitle) { with(binding.gradeStatisticsBarTitle) {
text = points.subject text = points.subject
visibility = if (items.size == 1) GONE else VISIBLE visibility = if (items.size == 1) GONE else VISIBLE
} }
val dataset = BarDataSet(listOf( val dataset = BarDataSet(
listOf(
BarEntry(1f, points.others.toFloat()), BarEntry(1f, points.others.toFloat()),
BarEntry(2f, points.student.toFloat()) BarEntry(2f, points.student.toFloat())
), binding.root.context.getString(R.string.grade_statistics_legend)) ), binding.root.context.getString(R.string.grade_statistics_legend)
)
with(dataset) { with(dataset) {
valueTextSize = 12f valueTextSize = 12f
@ -189,7 +260,8 @@ class GradeStatisticsAdapter @Inject constructor() :
form = Legend.LegendForm.SQUARE form = Legend.LegendForm.SQUARE
}, },
LegendEntry().apply { LegendEntry().apply {
label = binding.root.context.getString(R.string.grade_statistics_average_student) label =
binding.root.context.getString(R.string.grade_statistics_average_student)
formColor = gradePointsColors[1] formColor = gradePointsColors[1]
form = Legend.LegendForm.SQUARE form = Legend.LegendForm.SQUARE
} }
@ -226,4 +298,7 @@ class GradeStatisticsAdapter @Inject constructor() :
private class PointsViewHolder(val binding: ItemGradeStatisticsBarBinding) : private class PointsViewHolder(val binding: ItemGradeStatisticsBarBinding) :
RecyclerView.ViewHolder(binding.root) RecyclerView.ViewHolder(binding.root)
private class HeaderViewHolder(val binding: ItemGradeStatisticsHeaderBinding) :
RecyclerView.ViewHolder(binding.root)
} }

View File

@ -38,27 +38,28 @@ class GradeStatisticsFragment :
override val isViewEmpty get() = statisticsAdapter.items.isEmpty() override val isViewEmpty get() = statisticsAdapter.items.isEmpty()
override val currentType override val currentType get() = statisticsAdapter.currentDataType
get() = when (binding.gradeStatisticsTypeSwitch.checkedRadioButtonId) {
R.id.gradeStatisticsTypeSemester -> ViewType.SEMESTER
R.id.gradeStatisticsTypePartial -> ViewType.PARTIAL
else -> ViewType.POINTS
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
binding = FragmentGradeStatisticsBinding.bind(view) binding = FragmentGradeStatisticsBinding.bind(view)
messageContainer = binding.gradeStatisticsSwipe messageContainer = binding.gradeStatisticsSwipe
presenter.onAttachView(this, savedInstanceState?.getSerializable(SAVED_CHART_TYPE) as? ViewType) presenter.onAttachView(
this,
savedInstanceState?.getSerializable(SAVED_CHART_TYPE) as? GradeStatisticsItem.DataType
)
} }
override fun initView() { override fun initView() {
statisticsAdapter.onDataTypeChangeListener = presenter::onTypeChange
with(binding.gradeStatisticsRecycler) { with(binding.gradeStatisticsRecycler) {
layoutManager = LinearLayoutManager(requireContext()) layoutManager = LinearLayoutManager(requireContext())
adapter = statisticsAdapter adapter = statisticsAdapter
} }
subjectsAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, mutableListOf()) subjectsAdapter =
ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, mutableListOf())
subjectsAdapter.setDropDownViewResource(R.layout.item_attendance_summary_subject) subjectsAdapter.setDropDownViewResource(R.layout.item_attendance_summary_subject)
with(binding.gradeStatisticsSubjects) { with(binding.gradeStatisticsSubjects) {
@ -71,7 +72,9 @@ class GradeStatisticsFragment :
gradeStatisticsSwipe.setOnRefreshListener(presenter::onSwipeRefresh) gradeStatisticsSwipe.setOnRefreshListener(presenter::onSwipeRefresh)
gradeStatisticsSwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary)) gradeStatisticsSwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary))
gradeStatisticsSwipe.setProgressBackgroundColorSchemeColor(requireContext().getThemeAttrColor(R.attr.colorSwipeRefresh)) gradeStatisticsSwipe.setProgressBackgroundColorSchemeColor(
requireContext().getThemeAttrColor(R.attr.colorSwipeRefresh)
)
gradeStatisticsErrorRetry.setOnClickListener { presenter.onRetry() } gradeStatisticsErrorRetry.setOnClickListener { presenter.onRetry() }
gradeStatisticsErrorDetails.setOnClickListener { presenter.onDetailsClick() } gradeStatisticsErrorDetails.setOnClickListener { presenter.onDetailsClick() }
} }
@ -85,11 +88,15 @@ class GradeStatisticsFragment :
} }
} }
override fun updateData(items: List<GradeStatisticsItem>, theme: String, showAllSubjectsOnStatisticsList: Boolean) { override fun updateData(
newItems: List<GradeStatisticsItem>,
newTheme: String,
showAllSubjectsOnStatisticsList: Boolean
) {
with(statisticsAdapter) { with(statisticsAdapter) {
this.showAllSubjectsOnList = showAllSubjectsOnStatisticsList showAllSubjectsOnList = showAllSubjectsOnStatisticsList
this.theme = theme theme = newTheme
this.items = items items = newItems
notifyDataSetChanged() notifyDataSetChanged()
} }
} }
@ -103,11 +110,7 @@ class GradeStatisticsFragment :
} }
override fun resetView() { override fun resetView() {
binding.gradeStatisticsScroll.scrollTo(0, 0) binding.gradeStatisticsRecycler.scrollToPosition(0)
}
override fun showContent(show: Boolean) {
binding.gradeStatisticsRecycler.visibility = if (show) View.VISIBLE else View.GONE
} }
override fun showEmpty(show: Boolean) { override fun showEmpty(show: Boolean) {
@ -154,11 +157,6 @@ class GradeStatisticsFragment :
(parentFragment as? GradeFragment)?.onChildRefresh() (parentFragment as? GradeFragment)?.onChildRefresh()
} }
override fun onResume() {
super.onResume()
binding.gradeStatisticsTypeSwitch.setOnCheckedChangeListener { _, _ -> presenter.onTypeChange() }
}
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
outState.putSerializable(SAVED_CHART_TYPE, presenter.currentType) outState.putSerializable(SAVED_CHART_TYPE, presenter.currentType)

View File

@ -35,12 +35,12 @@ class GradeStatisticsPresenter @Inject constructor(
private lateinit var lastError: Throwable private lateinit var lastError: Throwable
var currentType: ViewType = ViewType.PARTIAL var currentType: GradeStatisticsItem.DataType = GradeStatisticsItem.DataType.PARTIAL
private set private set
fun onAttachView(view: GradeStatisticsView, type: ViewType?) { fun onAttachView(view: GradeStatisticsView, type: GradeStatisticsItem.DataType?) {
super.onAttachView(view) super.onAttachView(view)
currentType = type ?: ViewType.PARTIAL currentType = type ?: GradeStatisticsItem.DataType.PARTIAL
view.initView() view.initView()
errorHandler.showErrorMessage = ::showErrorViewOnError errorHandler.showErrorMessage = ::showErrorViewOnError
} }
@ -59,11 +59,11 @@ class GradeStatisticsPresenter @Inject constructor(
} }
fun onParentViewChangeSemester() { fun onParentViewChangeSemester() {
clearDataInView()
view?.run { view?.run {
showProgress(true) showProgress(true)
enableSwipe(false) enableSwipe(false)
showRefresh(false) showRefresh(false)
showContent(false)
showErrorView(false) showErrorView(false)
showEmpty(false) showEmpty(false)
clearView() clearView()
@ -90,8 +90,8 @@ class GradeStatisticsPresenter @Inject constructor(
fun onSubjectSelected(name: String?) { fun onSubjectSelected(name: String?) {
Timber.i("Select grade stats subject $name") Timber.i("Select grade stats subject $name")
clearDataInView()
view?.run { view?.run {
showContent(false)
showProgress(true) showProgress(true)
enableSwipe(false) enableSwipe(false)
showEmpty(false) showEmpty(false)
@ -104,11 +104,11 @@ class GradeStatisticsPresenter @Inject constructor(
} }
fun onTypeChange() { fun onTypeChange() {
val type = view?.currentType ?: ViewType.POINTS val type = view?.currentType ?: GradeStatisticsItem.DataType.POINTS
Timber.i("Select grade stats semester: $type") Timber.i("Select grade stats semester: $type")
cancelJobs("load") cancelJobs("load")
clearDataInView()
view?.run { view?.run {
showContent(false)
showProgress(true) showProgress(true)
enableSwipe(false) enableSwipe(false)
showEmpty(false) showEmpty(false)
@ -143,10 +143,16 @@ class GradeStatisticsPresenter @Inject constructor(
}.launch("subjects") }.launch("subjects")
} }
private fun loadDataByType(semesterId: Int, subjectName: String, type: ViewType, forceRefresh: Boolean = false) { private fun loadDataByType(
semesterId: Int,
subjectName: String,
type: GradeStatisticsItem.DataType,
forceRefresh: Boolean = false
) {
Timber.i("Loading grade stats data started") Timber.i("Loading grade stats data started")
currentSubjectName = if (preferencesRepository.showAllSubjectsOnStatisticsList) "Wszystkie" else subjectName currentSubjectName =
if (preferencesRepository.showAllSubjectsOnStatisticsList) "Wszystkie" else subjectName
currentType = type currentType = type
flowWithResourceIn { flowWithResourceIn {
@ -156,9 +162,30 @@ class GradeStatisticsPresenter @Inject constructor(
with(gradeStatisticsRepository) { with(gradeStatisticsRepository) {
when (type) { when (type) {
ViewType.PARTIAL -> getGradesPartialStatistics(student, semester, currentSubjectName, forceRefresh) GradeStatisticsItem.DataType.PARTIAL -> {
ViewType.SEMESTER -> getGradesSemesterStatistics(student, semester, currentSubjectName, forceRefresh) getGradesPartialStatistics(
ViewType.POINTS -> getGradesPointsStatistics(student, semester, currentSubjectName, forceRefresh) student = student,
semester = semester,
subjectName = currentSubjectName,
forceRefresh = forceRefresh
)
}
GradeStatisticsItem.DataType.SEMESTER -> {
getGradesSemesterStatistics(
student = student,
semester = semester,
subjectName = currentSubjectName,
forceRefresh = forceRefresh
)
}
GradeStatisticsItem.DataType.POINTS -> {
getGradesPointsStatistics(
student = student,
semester = semester,
subjectName = currentSubjectName,
forceRefresh = forceRefresh
)
}
} }
} }
}.onEach { }.onEach {
@ -168,12 +195,15 @@ class GradeStatisticsPresenter @Inject constructor(
if (!isNoContent) { if (!isNoContent) {
view?.run { view?.run {
showEmpty(isNoContent) showEmpty(isNoContent)
showContent(!isNoContent)
showErrorView(false) showErrorView(false)
enableSwipe(true) enableSwipe(true)
showRefresh(true) showRefresh(true)
showProgress(false) showProgress(false)
updateData(it.data!!, preferencesRepository.gradeColorTheme, preferencesRepository.showAllSubjectsOnStatisticsList) updateData(
if (isNoContent) emptyList() else it.data!!,
preferencesRepository.gradeColorTheme,
preferencesRepository.showAllSubjectsOnStatisticsList
)
showSubjects(!preferencesRepository.showAllSubjectsOnStatisticsList) showSubjects(!preferencesRepository.showAllSubjectsOnStatisticsList)
} }
} }
@ -183,9 +213,12 @@ class GradeStatisticsPresenter @Inject constructor(
view?.run { view?.run {
val isNoContent = checkIsNoContent(it.data!!, type) val isNoContent = checkIsNoContent(it.data!!, type)
showEmpty(isNoContent) showEmpty(isNoContent)
showContent(!isNoContent)
showErrorView(false) showErrorView(false)
updateData(it.data, preferencesRepository.gradeColorTheme, preferencesRepository.showAllSubjectsOnStatisticsList) updateData(
if (isNoContent) emptyList() else it.data,
preferencesRepository.gradeColorTheme,
preferencesRepository.showAllSubjectsOnStatisticsList
)
showSubjects(!preferencesRepository.showAllSubjectsOnStatisticsList) showSubjects(!preferencesRepository.showAllSubjectsOnStatisticsList)
} }
analytics.logEvent( analytics.logEvent(
@ -209,14 +242,29 @@ class GradeStatisticsPresenter @Inject constructor(
}.launch("load") }.launch("load")
} }
private fun checkIsNoContent(items: List<GradeStatisticsItem>, type: ViewType): Boolean { private fun checkIsNoContent(
items: List<GradeStatisticsItem>,
type: GradeStatisticsItem.DataType
): Boolean {
return items.isEmpty() || when (type) { return items.isEmpty() || when (type) {
ViewType.SEMESTER -> items.firstOrNull()?.semester?.amounts.orEmpty().sum() == 0 GradeStatisticsItem.DataType.SEMESTER -> {
ViewType.PARTIAL -> items.firstOrNull()?.partial?.classAmounts.orEmpty().sum() == 0 items.firstOrNull()?.semester?.amounts.orEmpty().sum() == 0
ViewType.POINTS -> items.firstOrNull()?.points?.let { points ->
points.student == .0 && points.others == .0
} ?: false
} }
GradeStatisticsItem.DataType.PARTIAL -> {
items.firstOrNull()?.partial?.classAmounts.orEmpty().sum() == 0
}
GradeStatisticsItem.DataType.POINTS -> {
items.firstOrNull()?.points?.let { points -> points.student == .0 && points.others == .0 } ?: false
}
}
}
private fun clearDataInView() {
view?.updateData(
emptyList(),
preferencesRepository.gradeColorTheme,
preferencesRepository.showAllSubjectsOnStatisticsList
)
} }
private fun showErrorViewOnError(message: String, error: Throwable) { private fun showErrorViewOnError(message: String, error: Throwable) {

View File

@ -7,13 +7,17 @@ interface GradeStatisticsView : BaseView {
val isViewEmpty: Boolean val isViewEmpty: Boolean
val currentType: ViewType val currentType: GradeStatisticsItem.DataType
fun initView() fun initView()
fun updateSubjects(data: ArrayList<String>) fun updateSubjects(data: ArrayList<String>)
fun updateData(items: List<GradeStatisticsItem>, theme: String, showAllSubjectsOnStatisticsList: Boolean) fun updateData(
newItems: List<GradeStatisticsItem>,
newTheme: String,
showAllSubjectsOnStatisticsList: Boolean
)
fun showSubjects(show: Boolean) fun showSubjects(show: Boolean)
@ -25,8 +29,6 @@ interface GradeStatisticsView : BaseView {
fun resetView() fun resetView()
fun showContent(show: Boolean)
fun showEmpty(show: Boolean) fun showEmpty(show: Boolean)
fun showErrorView(show: Boolean) fun showErrorView(show: Boolean)

View File

@ -3,5 +3,6 @@ package io.github.wulkanowy.ui.modules.grade.statistics
enum class ViewType(val id: Int) { enum class ViewType(val id: Int) {
SEMESTER(1), SEMESTER(1),
PARTIAL(2), PARTIAL(2),
POINTS(3) POINTS(3),
HEADER(4)
} }

View File

@ -28,14 +28,13 @@ class HomeworkDetailsDialog : BaseDialogFragment<DialogHomeworkBinding>(), Homew
private lateinit var homework: Homework private lateinit var homework: Homework
companion object { companion object {
private const val ARGUMENT_KEY = "Item" private const val ARGUMENT_KEY = "Item"
fun newInstance(homework: Homework): HomeworkDetailsDialog { fun newInstance(homework: Homework) = HomeworkDetailsDialog().apply {
return HomeworkDetailsDialog().apply {
arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, homework) } arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, homework) }
} }
} }
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -45,19 +44,22 @@ class HomeworkDetailsDialog : BaseDialogFragment<DialogHomeworkBinding>(), Homew
} }
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(
return DialogHomeworkBinding.inflate(inflater).apply { binding = this }.root inflater: LayoutInflater,
} container: ViewGroup?,
savedInstanceState: Bundle?
) = DialogHomeworkBinding.inflate(inflater).apply { binding = this }.root
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onViewCreated(view, savedInstanceState)
presenter.onAttachView(this) presenter.onAttachView(this)
} }
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
override fun initView() { override fun initView() {
with(binding) { with(binding) {
homeworkDialogRead.text = view?.context?.getString(if (homework.isDone) R.string.homework_mark_as_undone else R.string.homework_mark_as_done) homeworkDialogRead.text =
view?.context?.getString(if (homework.isDone) R.string.homework_mark_as_undone else R.string.homework_mark_as_done)
homeworkDialogRead.setOnClickListener { presenter.toggleDone(homework) } homeworkDialogRead.setOnClickListener { presenter.toggleDone(homework) }
homeworkDialogClose.setOnClickListener { dismiss() } homeworkDialogClose.setOnClickListener { dismiss() }
} }
@ -87,7 +89,8 @@ class HomeworkDetailsDialog : BaseDialogFragment<DialogHomeworkBinding>(), Homew
} }
override fun updateMarkAsDoneLabel(isDone: Boolean) { override fun updateMarkAsDoneLabel(isDone: Boolean) {
binding.homeworkDialogRead.text = view?.context?.getString(if (isDone) R.string.homework_mark_as_undone else R.string.homework_mark_as_done) binding.homeworkDialogRead.text =
view?.context?.getString(if (isDone) R.string.homework_mark_as_undone else R.string.homework_mark_as_done)
} }
override fun onDestroyView() { override fun onDestroyView() {

View File

@ -52,6 +52,8 @@ class LoginActivity : BaseActivity<LoginPresenter, ActivityLoginBinding>(), Logi
updateHelper.onResume(this) updateHelper.onResume(this)
} }
//https://developer.android.com/guide/playcore/in-app-updates#status_callback
@Suppress("DEPRECATION")
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data) super.onActivityResult(requestCode, resultCode, data)
updateHelper.onActivityResult(requestCode, resultCode) updateHelper.onActivityResult(requestCode, resultCode)
@ -65,13 +67,15 @@ class LoginActivity : BaseActivity<LoginPresenter, ActivityLoginBinding>(), Logi
with(loginAdapter) { with(loginAdapter) {
containerId = binding.loginViewpager.id containerId = binding.loginViewpager.id
addFragments(listOf( addFragments(
listOf(
LoginFormFragment.newInstance(), LoginFormFragment.newInstance(),
LoginSymbolFragment.newInstance(), LoginSymbolFragment.newInstance(),
LoginStudentSelectFragment.newInstance(), LoginStudentSelectFragment.newInstance(),
LoginAdvancedFragment.newInstance(), LoginAdvancedFragment.newInstance(),
LoginRecoverFragment.newInstance() LoginRecoverFragment.newInstance()
)) )
)
} }
with(binding.loginViewpager) { with(binding.loginViewpager) {
@ -99,14 +103,20 @@ class LoginActivity : BaseActivity<LoginPresenter, ActivityLoginBinding>(), Logi
} }
override fun notifyInitSymbolFragment(loginData: Triple<String, String, String>) { override fun notifyInitSymbolFragment(loginData: Triple<String, String, String>) {
(loginAdapter.getFragmentInstance(1) as? LoginSymbolFragment)?.onParentInitSymbolFragment(loginData) (loginAdapter.getFragmentInstance(1) as? LoginSymbolFragment)?.onParentInitSymbolFragment(
loginData
)
} }
override fun notifyInitStudentSelectFragment(studentsWithSemesters: List<StudentWithSemesters>) { override fun notifyInitStudentSelectFragment(studentsWithSemesters: List<StudentWithSemesters>) {
(loginAdapter.getFragmentInstance(2) as? LoginStudentSelectFragment)?.onParentInitStudentSelectFragment(studentsWithSemesters) (loginAdapter.getFragmentInstance(2) as? LoginStudentSelectFragment)
?.onParentInitStudentSelectFragment(studentsWithSemesters)
} }
fun onFormFragmentAccountLogged(studentsWithSemesters: List<StudentWithSemesters>, loginData: Triple<String, String, String>) { fun onFormFragmentAccountLogged(
studentsWithSemesters: List<StudentWithSemesters>,
loginData: Triple<String, String, String>
) {
presenter.onFormViewAccountLogged(studentsWithSemesters, loginData) presenter.onFormViewAccountLogged(studentsWithSemesters, loginData)
} }

View File

@ -150,6 +150,13 @@ class LoginFormFragment : BaseFragment<FragmentLoginFormBinding>(R.layout.fragme
} }
} }
override fun setErrorEmailInvalid(domain: String) {
with(binding.loginFormUsernameLayout) {
requestFocus()
error = getString(R.string.login_invalid_custom_email,domain)
}
}
override fun clearUsernameError() { override fun clearUsernameError() {
binding.loginFormUsernameLayout.error = null binding.loginFormUsernameLayout.error = null
} }

View File

@ -11,6 +11,7 @@ import io.github.wulkanowy.utils.flowWithResource
import io.github.wulkanowy.utils.ifNullOrBlank import io.github.wulkanowy.utils.ifNullOrBlank
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import timber.log.Timber import timber.log.Timber
import java.net.URL
import javax.inject.Inject import javax.inject.Inject
class LoginFormPresenter @Inject constructor( class LoginFormPresenter @Inject constructor(
@ -87,7 +88,14 @@ class LoginFormPresenter @Inject constructor(
if (!validateCredentials(email, password, host)) return if (!validateCredentials(email, password, host)) return
flowWithResource { studentRepository.getStudentsScrapper(email, password, host, symbol) }.onEach { flowWithResource {
studentRepository.getStudentsScrapper(
email,
password,
host,
symbol
)
}.onEach {
when (it.status) { when (it.status) {
Status.LOADING -> view?.run { Status.LOADING -> view?.run {
Timber.i("Login started") Timber.i("Login started")
@ -150,11 +158,18 @@ class LoginFormPresenter @Inject constructor(
view?.setErrorLoginRequired() view?.setErrorLoginRequired()
isCorrect = false isCorrect = false
} }
if ("@" !in login && "email" in host) { if ("@" !in login && "email" in host) {
view?.setErrorEmailRequired() view?.setErrorEmailRequired()
isCorrect = false isCorrect = false
} }
if ("@" in login && "login" !in host && "email" !in host) {
val emailHost = login.substringAfter("@")
val emailDomain = URL(host).host
if (emailHost != emailDomain) {
view?.setErrorEmailInvalid(domain = emailDomain)
isCorrect = false
}
}
} }
if (password.isEmpty()) { if (password.isEmpty()) {

View File

@ -39,6 +39,8 @@ interface LoginFormView : BaseView {
fun setErrorPassIncorrect() fun setErrorPassIncorrect()
fun setErrorEmailInvalid(domain: String)
fun clearUsernameError() fun clearUsernameError()
fun clearPassError() fun clearPassError()

View File

@ -9,6 +9,8 @@ import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.LuckyNumber import io.github.wulkanowy.data.db.entities.LuckyNumber
import io.github.wulkanowy.databinding.FragmentLuckyNumberBinding import io.github.wulkanowy.databinding.FragmentLuckyNumberBinding
import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.luckynumber.history.LuckyNumberHistoryFragment
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.getThemeAttrColor import io.github.wulkanowy.utils.getThemeAttrColor
import javax.inject.Inject import javax.inject.Inject
@ -42,6 +44,7 @@ class LuckyNumberFragment :
luckyNumberSwipe.setOnRefreshListener(presenter::onSwipeRefresh) luckyNumberSwipe.setOnRefreshListener(presenter::onSwipeRefresh)
luckyNumberSwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary)) luckyNumberSwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary))
luckyNumberSwipe.setProgressBackgroundColorSchemeColor(requireContext().getThemeAttrColor(R.attr.colorSwipeRefresh)) luckyNumberSwipe.setProgressBackgroundColorSchemeColor(requireContext().getThemeAttrColor(R.attr.colorSwipeRefresh))
luckyNumberHistoryButton.setOnClickListener { openLuckyNumberHistory() }
luckyNumberErrorRetry.setOnClickListener { presenter.onRetry() } luckyNumberErrorRetry.setOnClickListener { presenter.onRetry() }
luckyNumberErrorDetails.setOnClickListener { presenter.onDetailsClick() } luckyNumberErrorDetails.setOnClickListener { presenter.onDetailsClick() }
} }
@ -79,6 +82,10 @@ class LuckyNumberFragment :
binding.luckyNumberContent.visibility = if (show) VISIBLE else GONE binding.luckyNumberContent.visibility = if (show) VISIBLE else GONE
} }
override fun openLuckyNumberHistory() {
(activity as? MainActivity)?.pushView(LuckyNumberHistoryFragment.newInstance())
}
override fun onDestroyView() { override fun onDestroyView() {
presenter.onDetachView() presenter.onDetachView()
super.onDestroyView() super.onDestroyView()

View File

@ -24,4 +24,6 @@ interface LuckyNumberView : BaseView {
fun enableSwipe(enable: Boolean) fun enableSwipe(enable: Boolean)
fun showContent(show: Boolean) fun showContent(show: Boolean)
fun openLuckyNumberHistory()
} }

View File

@ -0,0 +1,36 @@
package io.github.wulkanowy.ui.modules.luckynumber.history
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import io.github.wulkanowy.data.db.entities.LuckyNumber
import io.github.wulkanowy.databinding.ItemLuckyNumberHistoryBinding
import io.github.wulkanowy.utils.toFormattedString
import io.github.wulkanowy.utils.weekDayName
import java.util.Locale
import javax.inject.Inject
class LuckyNumberHistoryAdapter @Inject constructor() :
RecyclerView.Adapter<LuckyNumberHistoryAdapter.ItemViewHolder>() {
var items = emptyList<LuckyNumber>()
override fun getItemCount() = items.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ItemViewHolder(
ItemLuckyNumberHistoryBinding.inflate(LayoutInflater.from(parent.context), parent, false)
)
@SuppressLint("DefaultLocale")
override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
val item = items[position]
with(holder.binding) {
luckyNumberHistoryWeekName.text = item.date.weekDayName.capitalize()
luckyNumberHistoryDate.text = item.date.toFormattedString()
luckyNumberHistory.text = item.luckyNumber.toString()
}
}
class ItemViewHolder(val binding: ItemLuckyNumberHistoryBinding) : RecyclerView.ViewHolder(binding.root)
}

View File

@ -0,0 +1,134 @@
package io.github.wulkanowy.ui.modules.luckynumber.history
import android.os.Bundle
import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import androidx.recyclerview.widget.LinearLayoutManager
import com.wdullaer.materialdatetimepicker.date.DatePickerDialog
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.LuckyNumber
import io.github.wulkanowy.databinding.FragmentLuckyNumberHistoryBinding
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.ui.widgets.DividerItemDecoration
import io.github.wulkanowy.utils.SchooldaysRangeLimiter
import io.github.wulkanowy.utils.dpToPx
import io.github.wulkanowy.utils.getThemeAttrColor
import java.time.LocalDate
import javax.inject.Inject
@AndroidEntryPoint
class LuckyNumberHistoryFragment :
BaseFragment<FragmentLuckyNumberHistoryBinding>(R.layout.fragment_lucky_number_history), LuckyNumberHistoryView,
MainView.TitledView {
@Inject
lateinit var presenter: LuckyNumberHistoryPresenter
@Inject
lateinit var luckyNumberHistoryAdapter: LuckyNumberHistoryAdapter
companion object {
fun newInstance() = LuckyNumberHistoryFragment()
}
override val titleStringId: Int
get() = R.string.lucky_number_history_title
override val isViewEmpty get() = luckyNumberHistoryAdapter.items.isEmpty()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentLuckyNumberHistoryBinding.bind(view)
messageContainer = binding.luckyNumberHistoryRecycler
presenter.onAttachView(this)
}
override fun initView() {
with(binding.luckyNumberHistoryRecycler) {
layoutManager = LinearLayoutManager(context)
adapter = luckyNumberHistoryAdapter
addItemDecoration(DividerItemDecoration(context))
}
with(binding) {
luckyNumberHistoryNavDate.setOnClickListener { presenter.onPickDate() }
luckyNumberHistoryErrorRetry.setOnClickListener { presenter.onRetry() }
luckyNumberHistoryErrorDetails.setOnClickListener { presenter.onDetailsClick() }
luckyNumberHistoryPreviousButton.setOnClickListener { presenter.onPreviousWeek() }
luckyNumberHistoryNextButton.setOnClickListener { presenter.onNextWeek() }
luckyNumberHistoryNavContainer.setElevationCompat(requireContext().dpToPx(8f))
}
}
override fun updateData(data: List<LuckyNumber>) {
with(luckyNumberHistoryAdapter) {
items = data
notifyDataSetChanged()
}
}
override fun clearData() {
with(luckyNumberHistoryAdapter) {
items = emptyList()
notifyDataSetChanged()
}
}
override fun showEmpty(show: Boolean) {
binding.luckyNumberHistoryEmpty.visibility = if (show) VISIBLE else GONE
}
override fun showErrorView(show: Boolean) {
binding.luckyNumberHistoryError.visibility = if (show) VISIBLE else GONE
}
override fun setErrorDetails(message: String) {
binding.luckyNumberHistoryErrorMessage.text = message
}
override fun updateNavigationWeek(date: String) {
binding.luckyNumberHistoryNavDate.text = date
}
override fun showProgress(show: Boolean) {
binding.luckyNumberHistoryProgress.visibility = if (show) VISIBLE else GONE
}
override fun showPreButton(show: Boolean) {
binding.luckyNumberHistoryPreviousButton.visibility = if (show) VISIBLE else View.INVISIBLE
}
override fun showNextButton(show: Boolean) {
binding.luckyNumberHistoryNextButton.visibility = if (show) VISIBLE else View.INVISIBLE
}
override fun showDatePickerDialog(currentDate: LocalDate) {
val dateSetListener = DatePickerDialog.OnDateSetListener { _, year, month, dayOfMonth ->
presenter.onDateSet(year, month + 1, dayOfMonth)
}
val datePickerDialog = DatePickerDialog.newInstance(dateSetListener,
currentDate.year, currentDate.monthValue - 1, currentDate.dayOfMonth)
with(datePickerDialog) {
setDateRangeLimiter(SchooldaysRangeLimiter())
version = DatePickerDialog.Version.VERSION_2
scrollOrientation = DatePickerDialog.ScrollOrientation.VERTICAL
vibrate(false)
show(this@LuckyNumberHistoryFragment.parentFragmentManager, null)
}
}
override fun showContent(show: Boolean) {
binding.luckyNumberHistoryRecycler.visibility = if (show) VISIBLE else GONE
}
override fun onDestroyView() {
presenter.onDetachView()
super.onDestroyView()
}
}

View File

@ -0,0 +1,151 @@
package io.github.wulkanowy.ui.modules.luckynumber.history
import io.github.wulkanowy.data.Status
import io.github.wulkanowy.data.repositories.LuckyNumberRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.utils.AnalyticsHelper
import io.github.wulkanowy.utils.afterLoading
import io.github.wulkanowy.utils.flowWithResource
import io.github.wulkanowy.utils.isHolidays
import io.github.wulkanowy.utils.monday
import io.github.wulkanowy.utils.previousOrSameSchoolDay
import io.github.wulkanowy.utils.sunday
import io.github.wulkanowy.utils.toFormattedString
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.onEach
import timber.log.Timber
import java.time.LocalDate
import javax.inject.Inject
class LuckyNumberHistoryPresenter @Inject constructor(
errorHandler: ErrorHandler,
studentRepository: StudentRepository,
private val luckyNumberRepository: LuckyNumberRepository,
private val analytics: AnalyticsHelper
) : BasePresenter<LuckyNumberHistoryView>(errorHandler, studentRepository) {
private lateinit var lastError: Throwable
var currentDate: LocalDate = LocalDate.now().previousOrSameSchoolDay
override fun onAttachView(view: LuckyNumberHistoryView) {
super.onAttachView(view)
view.run {
initView()
reloadNavigation()
showContent(false)
}
Timber.i("Lucky number history view was initialized")
errorHandler.showErrorMessage = ::showErrorViewOnError
loadData()
}
private fun loadData() {
flowWithResource {
val student = studentRepository.getCurrentStudent()
luckyNumberRepository.getLuckyNumberHistory(student, currentDate.monday, currentDate.sunday)
}.onEach {
when (it.status) {
Status.LOADING -> Timber.i("Loading lucky number history started")
Status.SUCCESS -> {
if (!it.data?.first().isNullOrEmpty()) {
Timber.i("Loading lucky number result: Success")
view?.apply {
updateData(it.data!!.first())
showContent(true)
showEmpty(false)
showErrorView(false)
showProgress(false)
}
analytics.logEvent(
"load_items",
"type" to "lucky_number_history",
"numbers" to it.data
)
} else {
Timber.i("Loading lucky number history result: No lucky numbers found")
view?.run {
showContent(false)
showEmpty(true)
showErrorView(false)
}
}
}
Status.ERROR -> {
Timber.i("Loading lucky number history result: An exception occurred")
errorHandler.dispatch(it.error!!)
}
}
}.afterLoading {
view?.run {
showProgress(false)
}
}.launch()
}
private fun showErrorViewOnError(message: String, error: Throwable) {
view?.run {
if (isViewEmpty) {
lastError = error
setErrorDetails(message)
showErrorView(true)
showEmpty(false)
} else showError(message, error)
}
}
private fun reloadView(date: LocalDate) {
currentDate = date
Timber.i("Reload lucky number history view with the date ${currentDate.toFormattedString()}")
view?.apply {
showProgress(true)
showContent(false)
showEmpty(false)
showErrorView(false)
clearData()
reloadNavigation()
}
}
fun onRetry() {
view?.run {
showErrorView(false)
showProgress(true)
}
loadData()
}
fun onDetailsClick() {
view?.showErrorDetailsDialog(lastError)
}
private fun reloadNavigation() {
view?.apply {
showPreButton(!currentDate.minusDays(7).isHolidays)
showNextButton(!currentDate.plusDays(7).isHolidays)
updateNavigationWeek("${currentDate.monday.toFormattedString("dd.MM")} - " +
currentDate.sunday.toFormattedString("dd.MM"))
}
}
fun onDateSet(year: Int, month: Int, day: Int) {
reloadView(LocalDate.of(year, month, day))
loadData()
}
fun onPickDate() {
view?.showDatePickerDialog(currentDate)
}
fun onPreviousWeek() {
reloadView(currentDate.minusDays(7))
loadData()
}
fun onNextWeek() {
reloadView(currentDate.plusDays(7))
loadData()
}
}

View File

@ -0,0 +1,36 @@
package io.github.wulkanowy.ui.modules.luckynumber.history
import io.github.wulkanowy.data.db.entities.LuckyNumber
import io.github.wulkanowy.ui.base.BaseView
import java.time.LocalDate
interface LuckyNumberHistoryView : BaseView {
val isViewEmpty: Boolean
fun initView()
fun updateData(data: List<LuckyNumber>)
fun clearData()
fun showEmpty(show: Boolean)
fun showErrorView(show: Boolean)
fun setErrorDetails(message: String)
fun updateNavigationWeek(date: String)
fun showProgress(show: Boolean)
fun showPreButton(show: Boolean)
fun showNextButton(show: Boolean)
fun showDatePickerDialog(currentDate: LocalDate)
fun showContent(show: Boolean)
fun onDestroyView()
}

View File

@ -11,15 +11,21 @@ import android.graphics.drawable.Icon
import android.os.Build import android.os.Build
import android.os.Build.VERSION.SDK_INT import android.os.Build.VERSION.SDK_INT
import android.os.Build.VERSION_CODES.LOLLIPOP import android.os.Build.VERSION_CODES.LOLLIPOP
import android.os.Build.VERSION_CODES.P
import android.os.Bundle import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.ViewGroup
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.core.view.updateMargins
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import com.aurelhubert.ahbottomnavigation.AHBottomNavigation.TitleState.ALWAYS_SHOW import com.aurelhubert.ahbottomnavigation.AHBottomNavigation.TitleState.ALWAYS_SHOW
import com.aurelhubert.ahbottomnavigation.AHBottomNavigationItem import com.aurelhubert.ahbottomnavigation.AHBottomNavigationItem
import com.google.android.material.elevation.ElevationOverlayProvider import com.google.android.material.elevation.ElevationOverlayProvider
@ -27,6 +33,8 @@ import com.ncapdevi.fragnav.FragNavController
import com.ncapdevi.fragnav.FragNavController.Companion.HIDE import com.ncapdevi.fragnav.FragNavController.Companion.HIDE
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.databinding.ActivityMainBinding import io.github.wulkanowy.databinding.ActivityMainBinding
import io.github.wulkanowy.ui.base.BaseActivity import io.github.wulkanowy.ui.base.BaseActivity
import io.github.wulkanowy.ui.modules.account.accountquick.AccountQuickDialog import io.github.wulkanowy.ui.modules.account.accountquick.AccountQuickDialog
@ -42,15 +50,18 @@ import io.github.wulkanowy.ui.modules.timetable.TimetableFragment
import io.github.wulkanowy.utils.AnalyticsHelper import io.github.wulkanowy.utils.AnalyticsHelper
import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.UpdateHelper import io.github.wulkanowy.utils.UpdateHelper
import io.github.wulkanowy.utils.createNameInitialsDrawable
import io.github.wulkanowy.utils.dpToPx import io.github.wulkanowy.utils.dpToPx
import io.github.wulkanowy.utils.getThemeAttrColor import io.github.wulkanowy.utils.getThemeAttrColor
import io.github.wulkanowy.utils.nickOrName
import io.github.wulkanowy.utils.safelyPopFragments import io.github.wulkanowy.utils.safelyPopFragments
import io.github.wulkanowy.utils.setOnViewChangeListener import io.github.wulkanowy.utils.setOnViewChangeListener
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainView { class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainView,
PreferenceFragmentCompat.OnPreferenceStartFragmentCallback {
@Inject @Inject
override lateinit var presenter: MainPresenter override lateinit var presenter: MainPresenter
@ -64,6 +75,8 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
@Inject @Inject
lateinit var appInfo: AppInfo lateinit var appInfo: AppInfo
private var accountMenu: MenuItem? = null
private val overlayProvider by lazy { ElevationOverlayProvider(this) } private val overlayProvider by lazy { ElevationOverlayProvider(this) }
private val navController = private val navController =
@ -121,6 +134,11 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
initialize(startMenuIndex, savedInstanceState) initialize(startMenuIndex, savedInstanceState)
pushFragment(moreMenuFragments[startMenuMoreIndex]) pushFragment(moreMenuFragments[startMenuMoreIndex])
} }
if (appInfo.systemVersion >= Build.VERSION_CODES.N_MR1) {
initShortcuts()
}
updateHelper.checkAndInstallUpdates(this) updateHelper.checkAndInstallUpdates(this)
} }
@ -129,11 +147,11 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
updateHelper.onResume(this) updateHelper.onResume(this)
} }
@SuppressLint("NewApi") //https://developer.android.com/guide/playcore/in-app-updates#status_callback
@Suppress("DEPRECATION")
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data) super.onActivityResult(requestCode, resultCode, data)
updateHelper.onActivityResult(requestCode, resultCode) updateHelper.onActivityResult(requestCode, resultCode)
if (appInfo.systemVersion >= Build.VERSION_CODES.N_MR1) initShortcuts()
} }
@RequiresApi(Build.VERSION_CODES.N_MR1) @RequiresApi(Build.VERSION_CODES.N_MR1)
@ -160,11 +178,6 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
getString(R.string.timetable_title), getString(R.string.timetable_title),
R.drawable.ic_shortcut_timetable, R.drawable.ic_shortcut_timetable,
MainView.Section.TIMETABLE MainView.Section.TIMETABLE
),
Triple(
getString(R.string.message_title),
R.drawable.ic_shortcut_message,
MainView.Section.MESSAGE
) )
).forEach { (title, icon, enum) -> ).forEach { (title, icon, enum) ->
shortcutsList.add( shortcutsList.add(
@ -191,9 +204,13 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
override fun onCreateOptionsMenu(menu: Menu?): Boolean { override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.action_menu_main, menu) menuInflater.inflate(R.menu.action_menu_main, menu)
accountMenu = menu?.findItem(R.id.mainMenuAccount)
presenter.onActionMenuCreated()
return true return true
} }
@SuppressLint("NewApi")
override fun initView() { override fun initView() {
with(binding.mainToolbar) { with(binding.mainToolbar) {
if (SDK_INT >= LOLLIPOP) stateListAnimator = null if (SDK_INT >= LOLLIPOP) stateListAnimator = null
@ -233,11 +250,25 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
with(navController) { with(navController) {
setOnViewChangeListener { section, name -> setOnViewChangeListener { section, name ->
binding.mainBottomNav.visibility =
if (section == MainView.Section.ACCOUNT || section == MainView.Section.STUDENT_INFO) { if (section == MainView.Section.ACCOUNT || section == MainView.Section.STUDENT_INFO) {
View.GONE binding.mainBottomNav.isVisible = false
binding.mainFragmentContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
updateMargins(bottom = 0)
}
if (appInfo.systemVersion >= P) {
window.navigationBarColor = getThemeAttrColor(R.attr.colorSurface)
}
} else { } else {
View.VISIBLE binding.mainBottomNav.isVisible = true
binding.mainFragmentContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
updateMargins(bottom = dpToPx(56f).toInt())
}
if (appInfo.systemVersion >= P) {
window.navigationBarColor =
getThemeAttrColor(android.R.attr.navigationBarColor)
}
} }
analytics.setCurrentScreen(this@MainActivity, name) analytics.setCurrentScreen(this@MainActivity, name)
@ -254,6 +285,16 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
} }
} }
override fun onPreferenceStartFragment(
caller: PreferenceFragmentCompat,
pref: Preference
): Boolean {
val fragment =
supportFragmentManager.fragmentFactory.instantiate(classLoader, pref.fragment)
navController.pushFragment(fragment)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
return if (item.itemId == R.id.mainMenuAccount) presenter.onAccountManagerSelected() return if (item.itemId == R.id.mainMenuAccount) presenter.onAccountManagerSelected()
else false else false
@ -280,8 +321,8 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
supportActionBar?.setDisplayHomeAsUpEnabled(show) supportActionBar?.setDisplayHomeAsUpEnabled(show)
} }
override fun showAccountPicker() { override fun showAccountPicker(studentWithSemesters: List<StudentWithSemesters>) {
navController.showDialogFragment(AccountQuickDialog.newInstance()) navController.showDialogFragment(AccountQuickDialog.newInstance(studentWithSemesters))
} }
override fun showActionBarElevation(show: Boolean) { override fun showActionBarElevation(show: Boolean) {
@ -315,6 +356,13 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
presenter.onBackPressed { super.onBackPressed() } presenter.onBackPressed { super.onBackPressed() }
} }
override fun showStudentAvatar(student: Student) {
accountMenu?.run {
icon = createNameInitialsDrawable(student.nickOrName, student.avatarColor, 0.44f)
title = getString(R.string.main_account_picker)
}
}
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
navController.onSaveInstanceState(outState) navController.onSaveInstanceState(outState)

View File

@ -1,5 +1,7 @@
package io.github.wulkanowy.ui.modules.main package io.github.wulkanowy.ui.modules.main
import io.github.wulkanowy.data.Status
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.services.sync.SyncManager import io.github.wulkanowy.services.sync.SyncManager
@ -9,6 +11,8 @@ import io.github.wulkanowy.ui.modules.main.MainView.Section.GRADE
import io.github.wulkanowy.ui.modules.main.MainView.Section.MESSAGE import io.github.wulkanowy.ui.modules.main.MainView.Section.MESSAGE
import io.github.wulkanowy.ui.modules.main.MainView.Section.SCHOOL import io.github.wulkanowy.ui.modules.main.MainView.Section.SCHOOL
import io.github.wulkanowy.utils.AnalyticsHelper import io.github.wulkanowy.utils.AnalyticsHelper
import io.github.wulkanowy.utils.flowWithResource
import kotlinx.coroutines.flow.onEach
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -17,9 +21,11 @@ class MainPresenter @Inject constructor(
studentRepository: StudentRepository, studentRepository: StudentRepository,
private val prefRepository: PreferencesRepository, private val prefRepository: PreferencesRepository,
private val syncManager: SyncManager, private val syncManager: SyncManager,
private val analytics: AnalyticsHelper private val analytics: AnalyticsHelper,
) : BasePresenter<MainView>(errorHandler, studentRepository) { ) : BasePresenter<MainView>(errorHandler, studentRepository) {
var studentsWitSemesters: List<StudentWithSemesters>? = null
fun onAttachView(view: MainView, initMenu: MainView.Section?) { fun onAttachView(view: MainView, initMenu: MainView.Section?) {
super.onAttachView(view) super.onAttachView(view)
view.apply { view.apply {
@ -35,6 +41,28 @@ class MainPresenter @Inject constructor(
analytics.logEvent("app_open", "destination" to initMenu?.name) analytics.logEvent("app_open", "destination" to initMenu?.name)
} }
fun onActionMenuCreated() {
if (!studentsWitSemesters.isNullOrEmpty()) {
showCurrentStudentAvatar()
return
}
flowWithResource { studentRepository.getSavedStudents(false) }
.onEach { resource ->
when (resource.status) {
Status.LOADING -> Timber.i("Loading student avatar data started")
Status.SUCCESS -> {
studentsWitSemesters = resource.data
showCurrentStudentAvatar()
}
Status.ERROR -> {
Timber.i("Loading student avatar result: An exception occurred")
errorHandler.dispatch(resource.error!!)
}
}
}.launch("avatar")
}
fun onViewChange(section: MainView.Section?) { fun onViewChange(section: MainView.Section?) {
view?.apply { view?.apply {
showActionBarElevation(section != GRADE && section != MESSAGE && section != SCHOOL) showActionBarElevation(section != GRADE && section != MESSAGE && section != SCHOOL)
@ -48,8 +76,10 @@ class MainPresenter @Inject constructor(
} }
fun onAccountManagerSelected(): Boolean { fun onAccountManagerSelected(): Boolean {
if (studentsWitSemesters.isNullOrEmpty()) return true
Timber.i("Select account manager") Timber.i("Select account manager")
view?.showAccountPicker() view?.showAccountPicker(studentsWitSemesters!!)
return true return true
} }
@ -81,6 +111,13 @@ class MainPresenter @Inject constructor(
} == true } == true
} }
private fun showCurrentStudentAvatar() {
val currentStudent =
studentsWitSemesters!!.single { it.student.isCurrent }.student
view?.showStudentAvatar(currentStudent)
}
private fun getProperViewIndexes(initMenu: MainView.Section?): Pair<Int, Int> { private fun getProperViewIndexes(initMenu: MainView.Section?): Pair<Int, Int> {
return when (initMenu?.id) { return when (initMenu?.id) {
in 0..3 -> initMenu!!.id to -1 in 0..3 -> initMenu!!.id to -1

View File

@ -1,5 +1,7 @@
package io.github.wulkanowy.ui.modules.main package io.github.wulkanowy.ui.modules.main
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.ui.base.BaseView import io.github.wulkanowy.ui.base.BaseView
interface MainView : BaseView { interface MainView : BaseView {
@ -22,7 +24,7 @@ interface MainView : BaseView {
fun showHomeArrow(show: Boolean) fun showHomeArrow(show: Boolean)
fun showAccountPicker() fun showAccountPicker(studentWithSemesters: List<StudentWithSemesters>)
fun showActionBarElevation(show: Boolean) fun showActionBarElevation(show: Boolean)
@ -36,6 +38,8 @@ interface MainView : BaseView {
fun popView(depth: Int = 1) fun popView(depth: Int = 1)
fun showStudentAvatar(student: Student)
interface MainChildView { interface MainChildView {
fun onFragmentReselected() fun onFragmentReselected()

View File

@ -20,13 +20,15 @@ import io.github.wulkanowy.ui.base.BaseDialogFragment
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class MobileDeviceTokenDialog : BaseDialogFragment<DialogMobileDeviceBinding>(), MobileDeviceTokenVIew { class MobileDeviceTokenDialog : BaseDialogFragment<DialogMobileDeviceBinding>(),
MobileDeviceTokenVIew {
@Inject @Inject
lateinit var presenter: MobileDeviceTokenPresenter lateinit var presenter: MobileDeviceTokenPresenter
companion object { companion object {
fun newInstance(): MobileDeviceTokenDialog = MobileDeviceTokenDialog()
fun newInstance() = MobileDeviceTokenDialog()
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -34,12 +36,14 @@ class MobileDeviceTokenDialog : BaseDialogFragment<DialogMobileDeviceBinding>(),
setStyle(STYLE_NO_TITLE, 0) setStyle(STYLE_NO_TITLE, 0)
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(
return DialogMobileDeviceBinding.inflate(inflater).apply { binding = this }.root inflater: LayoutInflater,
} container: ViewGroup?,
savedInstanceState: Bundle?
) = DialogMobileDeviceBinding.inflate(inflater).apply { binding = this }.root
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onViewCreated(view, savedInstanceState)
presenter.onAttachView(this) presenter.onAttachView(this)
} }

View File

@ -63,9 +63,6 @@ class MoreFragment : BaseFragment<FragmentMoreBinding>(R.layout.fragment_more),
override val settingsRes: Pair<String, Drawable?>? override val settingsRes: Pair<String, Drawable?>?
get() = context?.run { getString(R.string.settings_title) to getCompatDrawable(R.drawable.ic_more_settings) } get() = context?.run { getString(R.string.settings_title) to getCompatDrawable(R.drawable.ic_more_settings) }
override val aboutRes: Pair<String, Drawable?>?
get() = context?.run { getString(R.string.about_title) to getCompatDrawable(R.drawable.ic_all_about) }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
binding = FragmentMoreBinding.bind(view) binding = FragmentMoreBinding.bind(view)
@ -124,10 +121,6 @@ class MoreFragment : BaseFragment<FragmentMoreBinding>(R.layout.fragment_more),
(activity as? MainActivity)?.pushView(SettingsFragment.newInstance()) (activity as? MainActivity)?.pushView(SettingsFragment.newInstance())
} }
override fun openAboutView() {
(activity as? MainActivity)?.pushView(AboutFragment.newInstance())
}
override fun popView(depth: Int) { override fun popView(depth: Int) {
(activity as? MainActivity)?.popView(depth) (activity as? MainActivity)?.popView(depth)
} }

View File

@ -30,7 +30,6 @@ class MorePresenter @Inject constructor(
conferencesRes?.first -> openConferencesView() conferencesRes?.first -> openConferencesView()
schoolAndTeachersRes?.first -> openSchoolAndTeachersView() schoolAndTeachersRes?.first -> openSchoolAndTeachersView()
settingsRes?.first -> openSettingsView() settingsRes?.first -> openSettingsView()
aboutRes?.first -> openAboutView()
} }
} }
} }
@ -51,8 +50,7 @@ class MorePresenter @Inject constructor(
mobileDevicesRes, mobileDevicesRes,
conferencesRes, conferencesRes,
schoolAndTeachersRes, schoolAndTeachersRes,
settingsRes, settingsRes
aboutRes
)) ))
} }
} }

View File

@ -21,16 +21,12 @@ interface MoreView : BaseView {
val settingsRes: Pair<String, Drawable?>? val settingsRes: Pair<String, Drawable?>?
val aboutRes: Pair<String, Drawable?>?
fun initView() fun initView()
fun updateData(data: List<Pair<String, Drawable?>>) fun updateData(data: List<Pair<String, Drawable?>>)
fun openSettingsView() fun openSettingsView()
fun openAboutView()
fun popView(depth: Int) fun popView(depth: Int)
fun openMessagesView() fun openMessagesView()

View File

@ -22,14 +22,13 @@ class NoteDialog : DialogFragment() {
private lateinit var note: Note private lateinit var note: Note
companion object { companion object {
private const val ARGUMENT_KEY = "Item" private const val ARGUMENT_KEY = "Item"
fun newInstance(exam: Note): NoteDialog { fun newInstance(exam: Note) = NoteDialog().apply {
return NoteDialog().apply {
arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, exam) } arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, exam) }
} }
} }
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -39,13 +38,15 @@ class NoteDialog : DialogFragment() {
} }
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(
return DialogNoteBinding.inflate(inflater).apply { binding = this }.root inflater: LayoutInflater,
} container: ViewGroup?,
savedInstanceState: Bundle?
) = DialogNoteBinding.inflate(inflater).apply { binding = this }.root
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onViewCreated(view, savedInstanceState)
with(binding) { with(binding) {
noteDialogDate.text = note.date.toFormattedString() noteDialogDate.text = note.date.toFormattedString()
@ -57,11 +58,19 @@ class NoteDialog : DialogFragment() {
if (note.isPointsShow) { if (note.isPointsShow) {
with(binding.noteDialogPoints) { with(binding.noteDialogPoints) {
text = "${if (note.points > 0) "+" else ""}${note.points}" text = "${if (note.points > 0) "+" else ""}${note.points}"
setTextColor(when (NoteCategory.getByValue(note.categoryType)) { setTextColor(
NoteCategory.POSITIVE -> ContextCompat.getColor(requireContext(), R.color.note_positive) when (NoteCategory.getByValue(note.categoryType)) {
NoteCategory.NEGATIVE -> ContextCompat.getColor(requireContext(), R.color.note_negative) NoteCategory.POSITIVE -> ContextCompat.getColor(
requireContext(),
R.color.note_positive
)
NoteCategory.NEGATIVE -> ContextCompat.getColor(
requireContext(),
R.color.note_negative
)
else -> requireContext().getThemeAttrColor(android.R.attr.textColorPrimary) else -> requireContext().getThemeAttrColor(android.R.attr.textColorPrimary)
}) }
)
} }
} }

View File

@ -1,148 +1,22 @@
package io.github.wulkanowy.ui.modules.settings package io.github.wulkanowy.ui.modules.settings
import android.content.SharedPreferences
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.app.AlertDialog
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import com.thelittlefireman.appkillermanager.AppKillerManager
import com.thelittlefireman.appkillermanager.exceptions.NoActionFoundException
import com.yariksoffice.lingver.Lingver
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.ui.base.BaseActivity
import io.github.wulkanowy.ui.base.ErrorDialog
import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.AppInfo import timber.log.Timber
import io.github.wulkanowy.utils.openInternetBrowser
import javax.inject.Inject
@AndroidEntryPoint class SettingsFragment : PreferenceFragmentCompat(), MainView.TitledView {
class SettingsFragment : PreferenceFragmentCompat(),
SharedPreferences.OnSharedPreferenceChangeListener,
MainView.TitledView, SettingsView {
@Inject
lateinit var presenter: SettingsPresenter
@Inject
lateinit var appInfo: AppInfo
@Inject
lateinit var lingver: Lingver
companion object { companion object {
fun newInstance() = SettingsFragment() fun newInstance() = SettingsFragment()
} }
override val titleStringId get() = R.string.settings_title override val titleStringId get() = R.string.settings_title
override val syncSuccessString get() = getString(R.string.pref_services_message_sync_success)
override val syncFailedString get() = getString(R.string.pref_services_message_sync_failed)
override fun initView() {
findPreference<Preference>(getString(R.string.pref_key_services_force_sync))?.run {
onPreferenceClickListener = Preference.OnPreferenceClickListener {
presenter.onSyncNowClicked()
true
}
}
findPreference<Preference>(getString(R.string.pref_key_notifications_fix_issues))?.run {
isVisible = AppKillerManager.isDeviceSupported() && AppKillerManager.isAnyActionAvailable(requireContext())
setOnPreferenceClickListener {
presenter.onFixSyncIssuesClicked()
true
}
}
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
presenter.onAttachView(this)
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.scheme_preferences, rootKey) setPreferencesFromResource(R.xml.scheme_preferences, rootKey)
findPreference<Preference>(getString(R.string.pref_key_notification_debug))?.isVisible = appInfo.isDebug Timber.i("Settings view was initialized")
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
presenter.onSharedPreferenceChanged(key)
}
override fun recreateView() {
activity?.recreate()
}
override fun updateLanguage(langCode: String) {
lingver.setLocale(requireContext(), langCode)
}
override fun updateLanguageToFollowSystem() {
lingver.setFollowSystemLocale(requireContext())
}
override fun setServicesSuspended(serviceEnablesKey: String, isHolidays: Boolean) {
findPreference<Preference>(serviceEnablesKey)?.run {
summary = if (isHolidays) getString(R.string.pref_services_suspended) else ""
isEnabled = !isHolidays
}
}
override fun setSyncInProgress(inProgress: Boolean) {
if (activity == null || !isAdded) return
findPreference<Preference>(getString(R.string.pref_key_services_force_sync))?.run {
isEnabled = !inProgress
summary = if (inProgress) getString(R.string.pref_services_sync_in_progress) else ""
}
}
override fun showError(text: String, error: Throwable) {
(activity as? BaseActivity<*, *>)?.showError(text, error)
}
override fun showMessage(text: String) {
(activity as? BaseActivity<*, *>)?.showMessage(text)
}
override fun showExpiredDialog() {
(activity as? BaseActivity<*, *>)?.showExpiredDialog()
}
override fun openClearLoginView() {
(activity as? BaseActivity<*, *>)?.openClearLoginView()
}
override fun showErrorDetailsDialog(error: Throwable) {
ErrorDialog.newInstance(error).show(childFragmentManager, error.toString())
}
override fun showFixSyncDialog() {
AlertDialog.Builder(requireContext())
.setTitle(R.string.pref_notify_fix_sync_issues)
.setMessage(R.string.pref_notify_fix_sync_issues_message)
.setNegativeButton(android.R.string.cancel) { _, _ -> }
.setPositiveButton(R.string.pref_notify_fix_sync_issues_settings_button) { _, _ ->
try {
AppKillerManager.doActionPowerSaving(requireContext())
AppKillerManager.doActionAutoStart(requireContext())
AppKillerManager.doActionNotification(requireContext())
} catch (e: NoActionFoundException) {
requireContext().openInternetBrowser("https://dontkillmyapp.com/${AppKillerManager.getDevice()?.manufacturer}", ::showMessage)
}
}
.show()
}
override fun onResume() {
super.onResume()
preferenceScreen.sharedPreferences.registerOnSharedPreferenceChangeListener(this)
}
override fun onPause() {
super.onPause()
preferenceScreen.sharedPreferences.unregisterOnSharedPreferenceChangeListener(this)
} }
} }

View File

@ -0,0 +1,78 @@
package io.github.wulkanowy.ui.modules.settings.advanced
import android.content.SharedPreferences
import android.os.Bundle
import android.view.View
import androidx.preference.PreferenceFragmentCompat
import com.yariksoffice.lingver.Lingver
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.ui.base.BaseActivity
import io.github.wulkanowy.ui.base.ErrorDialog
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.AppInfo
import javax.inject.Inject
@AndroidEntryPoint
class AdvancedFragment : PreferenceFragmentCompat(),
SharedPreferences.OnSharedPreferenceChangeListener,
MainView.TitledView, AdvancedView {
@Inject
lateinit var presenter: AdvancedPresenter
@Inject
lateinit var appInfo: AppInfo
@Inject
lateinit var lingver: Lingver
companion object {
fun newInstance() = AdvancedFragment()
}
override val titleStringId get() = R.string.pref_settings_advanced_title
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
presenter.onAttachView(this)
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.scheme_preferences_advanced, rootKey)
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
presenter.onSharedPreferenceChanged(key)
}
override fun showError(text: String, error: Throwable) {
(activity as? BaseActivity<*, *>)?.showError(text, error)
}
override fun showMessage(text: String) {
(activity as? BaseActivity<*, *>)?.showMessage(text)
}
override fun showExpiredDialog() {
(activity as? BaseActivity<*, *>)?.showExpiredDialog()
}
override fun openClearLoginView() {
(activity as? BaseActivity<*, *>)?.openClearLoginView()
}
override fun showErrorDetailsDialog(error: Throwable) {
ErrorDialog.newInstance(error).show(childFragmentManager, error.toString())
}
override fun onResume() {
super.onResume()
preferenceScreen.sharedPreferences.registerOnSharedPreferenceChangeListener(this)
}
override fun onPause() {
super.onPause()
preferenceScreen.sharedPreferences.unregisterOnSharedPreferenceChangeListener(this)
}
}

View File

@ -0,0 +1,25 @@
package io.github.wulkanowy.ui.modules.settings.advanced
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.utils.AnalyticsHelper
import timber.log.Timber
import javax.inject.Inject
class AdvancedPresenter @Inject constructor(
errorHandler: ErrorHandler,
studentRepository: StudentRepository,
private val analytics: AnalyticsHelper,
) : BasePresenter<AdvancedView>(errorHandler, studentRepository) {
override fun onAttachView(view: AdvancedView) {
super.onAttachView(view)
Timber.i("Settings advanced view was initialized")
}
fun onSharedPreferenceChanged(key: String) {
Timber.i("Change settings $key")
analytics.logEvent("setting_changed", "name" to key)
}
}

View File

@ -0,0 +1,5 @@
package io.github.wulkanowy.ui.modules.settings.advanced
import io.github.wulkanowy.ui.base.BaseView
interface AdvancedView : BaseView {}

View File

@ -0,0 +1,90 @@
package io.github.wulkanowy.ui.modules.settings.appearance
import android.content.SharedPreferences
import android.os.Bundle
import android.view.View
import androidx.preference.PreferenceFragmentCompat
import com.yariksoffice.lingver.Lingver
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.ui.base.BaseActivity
import io.github.wulkanowy.ui.base.ErrorDialog
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.AppInfo
import javax.inject.Inject
@AndroidEntryPoint
class AppearanceFragment : PreferenceFragmentCompat(),
SharedPreferences.OnSharedPreferenceChangeListener,
MainView.TitledView, AppearanceView {
@Inject
lateinit var presenter: AppearancePresenter
@Inject
lateinit var appInfo: AppInfo
@Inject
lateinit var lingver: Lingver
companion object {
fun newInstance() = AppearanceFragment()
}
override val titleStringId get() = R.string.pref_settings_appearance_title
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
presenter.onAttachView(this)
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.scheme_preferences_appearance, rootKey)
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
presenter.onSharedPreferenceChanged(key)
}
override fun recreateView() {
activity?.recreate()
}
override fun updateLanguage(langCode: String) {
lingver.setLocale(requireContext(), langCode)
}
override fun updateLanguageToFollowSystem() {
lingver.setFollowSystemLocale(requireContext())
}
override fun showError(text: String, error: Throwable) {
(activity as? BaseActivity<*, *>)?.showError(text, error)
}
override fun showMessage(text: String) {
(activity as? BaseActivity<*, *>)?.showMessage(text)
}
override fun showExpiredDialog() {
(activity as? BaseActivity<*, *>)?.showExpiredDialog()
}
override fun openClearLoginView() {
(activity as? BaseActivity<*, *>)?.openClearLoginView()
}
override fun showErrorDetailsDialog(error: Throwable) {
ErrorDialog.newInstance(error).show(childFragmentManager, error.toString())
}
override fun onResume() {
super.onResume()
preferenceScreen.sharedPreferences.registerOnSharedPreferenceChangeListener(this)
}
override fun onPause() {
super.onPause()
preferenceScreen.sharedPreferences.unregisterOnSharedPreferenceChangeListener(this)
}
}

View File

@ -0,0 +1,45 @@
package io.github.wulkanowy.ui.modules.settings.appearance
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.utils.AnalyticsHelper
import io.github.wulkanowy.utils.AppInfo
import timber.log.Timber
import javax.inject.Inject
class AppearancePresenter @Inject constructor(
errorHandler: ErrorHandler,
studentRepository: StudentRepository,
private val preferencesRepository: PreferencesRepository,
private val analytics: AnalyticsHelper,
private val appInfo: AppInfo
) : BasePresenter<AppearanceView>(errorHandler, studentRepository) {
override fun onAttachView(view: AppearanceView) {
super.onAttachView(view)
Timber.i("Settings appearance view was initialized")
}
fun onSharedPreferenceChanged(key: String) {
Timber.i("Change settings $key")
preferencesRepository.apply {
when (key) {
appThemeKey -> view?.recreateView()
appLanguageKey -> view?.run {
if (appLanguage == "system") {
updateLanguageToFollowSystem()
analytics.logEvent("language", "setting_changed" to appInfo.systemLanguage)
} else {
updateLanguage(appLanguage)
analytics.logEvent("language", "setting_changed" to appLanguage)
}
recreateView()
}
}
}
analytics.logEvent("setting_changed", "name" to key)
}
}

View File

@ -0,0 +1,12 @@
package io.github.wulkanowy.ui.modules.settings.appearance
import io.github.wulkanowy.ui.base.BaseView
interface AppearanceView : BaseView {
fun recreateView()
fun updateLanguage(langCode: String)
fun updateLanguageToFollowSystem()
}

View File

@ -0,0 +1,130 @@
package io.github.wulkanowy.ui.modules.settings.notifications
import android.content.SharedPreferences
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AlertDialog
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import androidx.recyclerview.widget.RecyclerView
import com.thelittlefireman.appkillermanager.AppKillerManager
import com.thelittlefireman.appkillermanager.exceptions.NoActionFoundException
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.ui.base.BaseActivity
import io.github.wulkanowy.ui.base.ErrorDialog
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.openInternetBrowser
import javax.inject.Inject
@AndroidEntryPoint
class NotificationsFragment : PreferenceFragmentCompat(),
SharedPreferences.OnSharedPreferenceChangeListener,
MainView.TitledView, NotificationsView {
@Inject
lateinit var presenter: NotificationsPresenter
companion object {
fun newInstance() = NotificationsFragment()
}
override val titleStringId get() = R.string.pref_settings_notifications_title
override fun initView(showDebugNotificationSwitch: Boolean) {
findPreference<Preference>(getString(R.string.pref_key_notification_debug))?.isVisible =
showDebugNotificationSwitch
findPreference<Preference>(getString(R.string.pref_key_notifications_fix_issues))?.run {
isVisible = AppKillerManager.isDeviceSupported()
&& AppKillerManager.isAnyActionAvailable(requireContext())
setOnPreferenceClickListener {
presenter.onFixSyncIssuesClicked()
true
}
}
}
override fun onCreateRecyclerView(
inflater: LayoutInflater?,
parent: ViewGroup?,
state: Bundle?
): RecyclerView? = super.onCreateRecyclerView(inflater, parent, state)
.also {
it.itemAnimator = null
it.layoutAnimation = null
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
presenter.onAttachView(this)
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.scheme_preferences_notifications, rootKey)
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
presenter.onSharedPreferenceChanged(key)
}
override fun enableNotification(notificationKey: String, enable: Boolean) {
findPreference<Preference>(notificationKey)?.run {
isEnabled = enable
summary = if (enable) null else getString(R.string.pref_notify_disabled_summary)
}
}
override fun showError(text: String, error: Throwable) {
(activity as? BaseActivity<*, *>)?.showError(text, error)
}
override fun showMessage(text: String) {
(activity as? BaseActivity<*, *>)?.showMessage(text)
}
override fun showExpiredDialog() {
(activity as? BaseActivity<*, *>)?.showExpiredDialog()
}
override fun openClearLoginView() {
(activity as? BaseActivity<*, *>)?.openClearLoginView()
}
override fun showErrorDetailsDialog(error: Throwable) {
ErrorDialog.newInstance(error).show(childFragmentManager, error.toString())
}
override fun showFixSyncDialog() {
AlertDialog.Builder(requireContext())
.setTitle(R.string.pref_notify_fix_sync_issues)
.setMessage(R.string.pref_notify_fix_sync_issues_message)
.setNegativeButton(android.R.string.cancel) { _, _ -> }
.setPositiveButton(R.string.pref_notify_fix_sync_issues_settings_button) { _, _ ->
try {
AppKillerManager.doActionPowerSaving(requireContext())
AppKillerManager.doActionAutoStart(requireContext())
AppKillerManager.doActionNotification(requireContext())
} catch (e: NoActionFoundException) {
requireContext().openInternetBrowser(
"https://dontkillmyapp.com/${AppKillerManager.getDevice()?.manufacturer}",
::showMessage
)
}
}
.show()
}
override fun onResume() {
super.onResume()
preferenceScreen.sharedPreferences.registerOnSharedPreferenceChangeListener(this)
}
override fun onPause() {
super.onPause()
preferenceScreen.sharedPreferences.unregisterOnSharedPreferenceChangeListener(this)
}
}

View File

@ -0,0 +1,58 @@
package io.github.wulkanowy.ui.modules.settings.notifications
import com.chuckerteam.chucker.api.ChuckerCollector
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.services.alarm.TimetableNotificationSchedulerHelper
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.utils.AnalyticsHelper
import io.github.wulkanowy.utils.AppInfo
import timber.log.Timber
import javax.inject.Inject
class NotificationsPresenter @Inject constructor(
errorHandler: ErrorHandler,
studentRepository: StudentRepository,
private val preferencesRepository: PreferencesRepository,
private val timetableNotificationHelper: TimetableNotificationSchedulerHelper,
private val appInfo: AppInfo,
private val analytics: AnalyticsHelper,
private val chuckerCollector: ChuckerCollector
) : BasePresenter<NotificationsView>(errorHandler, studentRepository) {
override fun onAttachView(view: NotificationsView) {
super.onAttachView(view)
with(view) {
enableNotification(
preferencesRepository.notificationsEnableKey,
preferencesRepository.isServiceEnabled
)
initView(appInfo.isDebug)
}
Timber.i("Settings notifications view was initialized")
}
fun onSharedPreferenceChanged(key: String) {
Timber.i("Change settings $key")
preferencesRepository.apply {
when (key) {
isUpcomingLessonsNotificationsEnableKey -> {
if (!isUpcomingLessonsNotificationsEnable) {
timetableNotificationHelper.cancelNotification()
}
}
isDebugNotificationEnableKey -> {
chuckerCollector.showNotification = isDebugNotificationEnable
}
}
}
analytics.logEvent("setting_changed", "name" to key)
}
fun onFixSyncIssuesClicked() {
view?.showFixSyncDialog()
}
}

View File

@ -0,0 +1,12 @@
package io.github.wulkanowy.ui.modules.settings.notifications
import io.github.wulkanowy.ui.base.BaseView
interface NotificationsView : BaseView {
fun initView(showDebugNotificationSwitch: Boolean)
fun showFixSyncDialog()
fun enableNotification(notificationKey: String, enable: Boolean)
}

View File

@ -0,0 +1,100 @@
package io.github.wulkanowy.ui.modules.settings.sync
import android.content.SharedPreferences
import android.os.Bundle
import android.view.View
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.ui.base.BaseActivity
import io.github.wulkanowy.ui.base.ErrorDialog
import io.github.wulkanowy.ui.modules.main.MainView
import javax.inject.Inject
@AndroidEntryPoint
class SyncFragment : PreferenceFragmentCompat(),
SharedPreferences.OnSharedPreferenceChangeListener,
MainView.TitledView, SyncView {
@Inject
lateinit var presenter: SyncPresenter
companion object {
fun newInstance() = SyncFragment()
}
override val titleStringId get() = R.string.pref_settings_sync_title
override val syncSuccessString get() = getString(R.string.pref_services_message_sync_success)
override val syncFailedString get() = getString(R.string.pref_services_message_sync_failed)
override fun initView() {
findPreference<Preference>(getString(R.string.pref_key_services_force_sync))?.run {
onPreferenceClickListener = Preference.OnPreferenceClickListener {
presenter.onSyncNowClicked()
true
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
presenter.onAttachView(this)
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.scheme_preferences_sync, rootKey)
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
presenter.onSharedPreferenceChanged(key)
}
override fun setServicesSuspended(serviceEnablesKey: String, isHolidays: Boolean) {
findPreference<Preference>(serviceEnablesKey)?.run {
summary = if (isHolidays) getString(R.string.pref_services_suspended) else ""
isEnabled = !isHolidays
}
}
override fun setSyncInProgress(inProgress: Boolean) {
if (activity == null || !isAdded) return
findPreference<Preference>(getString(R.string.pref_key_services_force_sync))?.run {
isEnabled = !inProgress
summary = if (inProgress) getString(R.string.pref_services_sync_in_progress) else ""
}
}
override fun showError(text: String, error: Throwable) {
(activity as? BaseActivity<*, *>)?.showError(text, error)
}
override fun showMessage(text: String) {
(activity as? BaseActivity<*, *>)?.showMessage(text)
}
override fun showExpiredDialog() {
(activity as? BaseActivity<*, *>)?.showExpiredDialog()
}
override fun openClearLoginView() {
(activity as? BaseActivity<*, *>)?.openClearLoginView()
}
override fun showErrorDetailsDialog(error: Throwable) {
ErrorDialog.newInstance(error).show(childFragmentManager, error.toString())
}
override fun onResume() {
super.onResume()
preferenceScreen.sharedPreferences.registerOnSharedPreferenceChangeListener(this)
}
override fun onPause() {
super.onPause()
preferenceScreen.sharedPreferences.unregisterOnSharedPreferenceChangeListener(this)
}
}

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