1
0
Fork 1

Compare commits

..

No commits in common. "1.1.2" and "0.24.3" have entirely different histories.

303 changed files with 2052 additions and 18811 deletions

View file

@ -10,14 +10,39 @@ on:
workflow_dispatch:
jobs:
build:
name: Pre-build
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: fkirc/skip-duplicate-actions@master
- uses: actions/checkout@v2
- uses: gradle/wrapper-validation-action@v1
- 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*') }}
- 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: fkirc/skip-duplicate-actions@master
- uses: actions/checkout@v2
- uses: gradle/wrapper-validation-action@v1
- uses: actions/setup-java@v1
with:
java-version: 11
@ -27,6 +52,11 @@ jobs:
~/.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
run: |
./gradlew --build-cache -Pcoverage testFdroidDebugUnitTest --stacktrace
@ -35,12 +65,49 @@ jobs:
with:
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:
name: Deploy to google play
runs-on: ubuntu-latest
timeout-minutes: 10
environment: google-play
needs: [ unit-tests ]
needs: [ build, unit-tests, instrumentation-tests ]
if: github.event_name == 'push' && contains(github.ref, 'refs/tags/')
steps:
- uses: actions/checkout@v2
@ -53,6 +120,11 @@ jobs:
~/.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: Decrypt keys
env:
ENCRYPT_KEY: ${{ secrets.ENCRYPT_KEY }}
@ -68,135 +140,3 @@ jobs:
PLAY_SERVICE_ACCOUNT_EMAIL: ${{ secrets.PLAY_SERVICE_ACCOUNT_EMAIL }}
PLAY_STORE_PASSWORD: ${{ secrets.PLAY_STORE_PASSWORD }}
run: ./gradlew publishPlayRelease -PenableFirebase --stacktrace;
deploy-appcenter:
name: Deploy to App Center
runs-on: ubuntu-latest
timeout-minutes: 10
environment: app-center
if: github.ref != 'refs/heads/develop'
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*') }}
- name: Set run number with offset
env:
BUILD_NUMBER_OFFSET: ${{ secrets.BUILD_NUMBER_OFFSET }}
run: echo "RUN_NUMBER=$((GITHUB_RUN_NUMBER+BUILD_NUMBER_OFFSET))" >> $GITHUB_ENV
- name: Prepare build configuration
run: |
sed -i -e "s#applicationIdSuffix \".dev\"#applicationIdSuffix \".${GITHUB_HEAD_REF//[-.\/]/_}\"#" app/build.gradle
sed -i -e "s#.dev\"#.${GITHUB_HEAD_REF//[-.\/]/_}\"#" app/src/debug/google-services.json
sed -i -e "s#.dev\"#.${GITHUB_HEAD_REF//[-.\/]/_}\"#" app/src/debug/agconnect-services.json
sed -i -e '/versionNameSuffix/d' app/build.gradle
- name: Add signing config
run: |
cat >> app/build.gradle <<EOF
android.signingConfigs.debug {
storeFile file("bitrise.jks")
storePassword System.getenv("BITRISE_KEYSTORE_PASSWORD")
keyAlias System.getenv("BITRISE_KEY_ALIAS")
keyPassword System.getenv("BITRISE_KEY_PASSWORD")
}
EOF
- name: Decrypt keys
env:
BITRISE_ENCRYPT_KEY: ${{ secrets.BITRISE_ENCRYPT_KEY }}
run: |
gpg --yes --batch --passphrase=$BITRISE_ENCRYPT_KEY ./app/bitrise.jks.gpg
- name: Bump version
uses: chkfung/android-version-actions@v1.1
with:
gradlePath: app/build.gradle
versionCode: ${{ env.RUN_NUMBER }}
versionName: ${{ env.RUN_NUMBER }}-${{ github.head_ref }}
- name: Build apk
env:
BITRISE_KEYSTORE_PASSWORD: ${{ secrets.BITRISE_KEYSTORE_PASSWORD }}
BITRISE_KEY_ALIAS: ${{ secrets.BITRISE_KEY_ALIAS }}
BITRISE_KEY_PASSWORD: ${{ secrets.BITRISE_KEY_PASSWORD }}
run: ./gradlew assembleFdroidDebug --stacktrace
- name: Upload apk to github artifacts
uses: actions/upload-artifact@v2
with:
name: wulkanowyDEV-${{ env.RUN_NUMBER }}.apk
path: app/build/outputs/apk/fdroid/debug/app-fdroid-debug.apk
- name: Deploy to app center
uses: wzieba/AppCenter-Github-Action@v1
with:
appName: wulkanowy/wulkanowy
token: ${{ secrets.APP_CENTER_TOKEN }}
group: Testers
file: app/build/outputs/apk/fdroid/debug/app-fdroid-debug.apk
notifyTesters: true
debug: true
deploy-app-distribution:
name: Deploy to AppDistribution
runs-on: ubuntu-latest
timeout-minutes: 10
environment: app-distribution
if: github.ref == 'refs/heads/develop'
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*') }}
- name: Set run number with offset
env:
BUILD_NUMBER_OFFSET: ${{ secrets.BUILD_NUMBER_OFFSET }}
run: echo "RUN_NUMBER=$((GITHUB_RUN_NUMBER+BUILD_NUMBER_OFFSET))" >> $GITHUB_ENV
- name: Add signing config
run: |
cat >> app/build.gradle <<EOF
android.signingConfigs.debug {
storeFile file("bitrise.jks")
storePassword System.getenv("BITRISE_KEYSTORE_PASSWORD")
keyAlias System.getenv("BITRISE_KEY_ALIAS")
keyPassword System.getenv("BITRISE_KEY_PASSWORD")
}
EOF
- name: Decrypt keys
env:
BITRISE_ENCRYPT_KEY: ${{ secrets.BITRISE_ENCRYPT_KEY }}
BITRISE_SERVICES_ENCRYPT_KEY: ${{ secrets.BITRISE_SERVICES_ENCRYPT_KEY }}
run: |
gpg --yes --batch --passphrase=$BITRISE_SERVICES_ENCRYPT_KEY ./app/src/debug/google-services.json.gpg
gpg --yes --batch --passphrase=$BITRISE_ENCRYPT_KEY ./app/bitrise.jks.gpg
- name: Bump version
uses: chkfung/android-version-actions@v1.1
with:
gradlePath: app/build.gradle
versionCode: ${{ env.RUN_NUMBER }}
versionName: ${{ env.RUN_NUMBER }}
- name: Build apk
env:
BITRISE_KEYSTORE_PASSWORD: ${{ secrets.BITRISE_KEYSTORE_PASSWORD }}
BITRISE_KEY_ALIAS: ${{ secrets.BITRISE_KEY_ALIAS }}
BITRISE_KEY_PASSWORD: ${{ secrets.BITRISE_KEY_PASSWORD }}
run: ./gradlew assemblePlayDebug -PenableFirebase --stacktrace
- name: Upload apk to github artifacts
uses: actions/upload-artifact@v2
with:
name: wulkanowyDEV-${{ env.RUN_NUMBER }}-dev.apk
path: app/build/outputs/apk/play/debug/app-play-debug.apk
- name: Deploy to app distribution
uses: wzieba/Firebase-Distribution-Github-Action@v1
with:
appId: ${{ secrets.FIREBASE_APP_ID }}
token: ${{ secrets.FIREBASE_TOKEN }}
groups: discord
file: app/build/outputs/apk/play/debug/app-play-debug.apk

View file

@ -18,9 +18,18 @@
</option>
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
<option name="CONTINUATION_INDENT_IN_PARAMETER_LISTS" value="false" />
<option name="CONTINUATION_INDENT_IN_ARGUMENT_LISTS" value="false" />
<option name="CONTINUATION_INDENT_FOR_EXPRESSION_BODIES" value="false" />
<option name="CONTINUATION_INDENT_FOR_CHAINED_CALLS" value="false" />
<option name="CONTINUATION_INDENT_IN_SUPERTYPE_LISTS" value="false" />
<option name="CONTINUATION_INDENT_IN_IF_CONDITIONS" value="false" />
<option name="CONTINUATION_INDENT_IN_ELVIS" value="false" />
<option name="WRAP_ELVIS_EXPRESSIONS" value="0" />
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<MarkdownNavigatorCodeStyleSettings>
<option name="RIGHT_MARGIN" value="72" />
</MarkdownNavigatorCodeStyleSettings>
<codeStyleSettings language="XML">
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
@ -134,11 +143,13 @@
</arrangement>
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
<option name="KEEP_BLANK_LINES_IN_DECLARATIONS" value="1" />
<option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
<option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="0" />
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
<option name="METHOD_PARAMETERS_LPAREN_ON_NEXT_LINE" value="true" />
<option name="METHOD_PARAMETERS_RPAREN_ON_NEXT_LINE" value="true" />
<option name="EXTENDS_LIST_WRAP" value="1" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>

View file

@ -12,7 +12,7 @@ Unofficial android VULCAN UONET+ register client for both students and their par
## Features
* logging in using the email and password
* logging in using the email and password OR using token and pin
* functions from the register website:
* grades
* grade statistics
@ -25,19 +25,15 @@ Unofficial android VULCAN UONET+ register client for both students and their par
* homework
* notes
* lucky number
* additional lessons
* school conferences
* student and school information
* calculation of the average independently of school's preferences
* notifications, e.g. about a new grade
* support for multiple accounts with the ability to rename students
* dark and black (AMOLED) theme
* offline mode
* no ads
## Download
You can download the current version from the Google Play, F-Droid or Huawei AppGallery store
You can download the current beta 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"
alt="Get it on Google Play"
@ -64,9 +60,6 @@ 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.
For people interested in translating the application into different languages, we provide Crowdin
https://crowdin.com/project/wulkanowy2
## License
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
* logowanie za pomocą e-maila i hasła
* logowanie za pomocą e-maila i hasła LUB tokena i pinu
* funkcje ze strony internetowej dziennika:
* oceny
* statystyki ocen
@ -25,19 +25,15 @@ Nieoficjalny klient dziennika VULCAN UONET+ dla ucznia i rodzica
* zadania domowe
* uwagi
* szczęśliwy numerek
* dodatkowe lekcje
* zebrania w szkole
* informacje o uczniu i szkole
* obliczanie średniej niezależnie od preferencji szkoły
* powiadomienia np. o nowej ocenie
* obsługa wielu kont wraz z możliwością zmiany nazwy ucznia
* ciemny i czarny (AMOLED) motyw
* tryb offilne
* brak reklam
## Pobierz
Aktualną wersję możesz pobrać ze sklepu Google Play, F-Droid lub Huawei AppGallery
Aktualną wersję beta 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"
alt="Pobierz z Google Play"
@ -65,9 +61,6 @@ 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.
Dla osób zainteresowanych tłumaczeniem aplikacji na różne języki udostępniamy Crowdina
https://crowdin.com/project/wulkanowy2
## Licencja
Ten projekt udostępniany jest na licencji Apache License 2.0 - szczegóły w pliku [LICENSE](LICENSE)

Binary file not shown.

Binary file not shown.

View file

@ -5,30 +5,25 @@ apply plugin: 'dagger.hilt.android.plugin'
apply plugin: 'com.google.firebase.crashlytics'
apply plugin: 'com.github.triplet.play'
apply plugin: 'com.mikepenz.aboutlibraries.plugin'
apply plugin: 'com.google.gms.google-services'
apply plugin: 'com.huawei.agconnect'
apply from: 'jacoco.gradle'
apply from: 'sonarqube.gradle'
apply from: 'hooks.gradle'
android {
compileSdkVersion 30
buildToolsVersion '30.0.3'
buildToolsVersion '30.0.2'
defaultConfig {
applicationId "io.github.wulkanowy"
testApplicationId "io.github.tests.wulkanowy"
minSdkVersion 17
targetSdkVersion 30
versionCode 88
versionName "1.1.2"
versionCode 81
versionName "0.24.3"
multiDexEnabled true
resValue "string", "app_name", "Wulkanowy"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true
resValue "string", "app_name", "Wulkanowy"
buildConfigField "long", "BUILD_TIMESTAMP", String.valueOf(System.currentTimeMillis())
manifestPlaceholders = [
firebase_enabled: project.hasProperty("enableFirebase")
]
@ -43,8 +38,7 @@ android {
}
sourceSets {
// https://github.com/robolectric/robolectric/issues/3928#issuecomment-395309991
debug.assets.srcDirs += files("$projectDir/schemas".toString())
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
}
signingConfigs {
@ -106,10 +100,6 @@ android {
disable 'HardwareIds'
}
testOptions.unitTests {
includeAndroidResources = true
}
compileOptions {
coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_1_8
@ -117,7 +107,6 @@ android {
}
kotlinOptions {
useIR = true
jvmTarget = "1.8"
freeCompilerArgs += ["-Xopt-in=kotlin.RequiresOptIn", "-Xjvm-default=all"]
}
@ -136,32 +125,31 @@ play {
serviceAccountEmail = System.getenv("PLAY_SERVICE_ACCOUNT_EMAIL") ?: "jan@fakelog.cf"
serviceAccountCredentials = file('key.p12')
defaultToAppBundles = false
track = 'production'
track = 'alpha'
updatePriority = 5
}
ext {
work_manager = "2.5.0"
work_hilt = "1.0.0-beta01"
room = "2.3.0-beta03"
work_manager = "2.4.0"
room = "2.2.6"
chucker = "3.4.0"
mockk = "1.10.6"
mockk = "1.10.5"
moshi = "1.11.0"
}
dependencies {
implementation "io.github.wulkanowy:sdk:1.1.2"
implementation "io.github.wulkanowy:sdk:0.24.1"
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.1'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.3'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.2'
implementation "androidx.core:core-ktx:1.3.2"
implementation "androidx.activity:activity-ktx:1.2.1"
implementation "androidx.activity:activity-ktx:1.1.0"
implementation "androidx.appcompat:appcompat:1.2.0"
implementation "androidx.appcompat:appcompat-resources:1.2.0"
implementation "androidx.fragment:fragment-ktx:1.3.1"
implementation "androidx.fragment:fragment-ktx:1.2.5"
implementation "androidx.annotation:annotation:1.1.0"
implementation "androidx.multidex:multidex:2.0.1"
@ -171,15 +159,15 @@ dependencies {
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
implementation "androidx.constraintlayout:constraintlayout:2.0.4"
implementation "androidx.coordinatorlayout:coordinatorlayout:1.1.0"
implementation "com.google.android.material:material:1.3.0"
implementation "com.github.wulkanowy:material-chips-input:2.2.0"
implementation "com.google.android.material:material:1.2.1"
implementation "com.github.wulkanowy:material-chips-input:2.1.1"
implementation "com.github.PhilJay:MPAndroidChart:v3.1.0"
implementation 'com.mikhaellopez:circularimageview:4.2.0'
implementation "me.zhanghai.android.materialprogressbar:library:1.6.1"
implementation "androidx.work:work-runtime-ktx:$work_manager"
playImplementation "androidx.work:work-gcm:$work_manager"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.0"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0"
implementation "androidx.room:room-runtime:$room"
implementation "androidx.room:room-ktx:$room"
@ -187,12 +175,12 @@ dependencies {
implementation "com.google.dagger:hilt-android:$hilt_version"
kapt "com.google.dagger:hilt-android-compiler:$hilt_version"
implementation "androidx.hilt:hilt-work:$work_hilt"
kapt "androidx.hilt:hilt-compiler:$work_hilt"
implementation 'androidx.hilt:hilt-work:1.0.0-alpha02'
kapt 'androidx.hilt:hilt-compiler:1.0.0-alpha02'
implementation "com.aurelhubert:ahbottomnavigation:2.3.4"
implementation "com.ncapdevi:frag-nav:3.3.0"
implementation "com.github.YarikSOffice:lingver:1.3.0"
implementation "com.github.YarikSOffice:lingver:1.2.2"
implementation "com.squareup.moshi:moshi:$moshi"
implementation "com.squareup.moshi:moshi-adapters:$moshi"
@ -206,7 +194,7 @@ dependencies {
implementation "io.github.wulkanowy:AppKillerManager:3.0.0"
implementation 'me.xdrop:fuzzywuzzy:1.3.1'
playImplementation platform('com.google.firebase:firebase-bom:26.7.0')
playImplementation platform('com.google.firebase:firebase-bom:26.3.0')
playImplementation 'com.google.firebase:firebase-analytics-ktx'
playImplementation 'com.google.firebase:firebase-inappmessaging-display-ktx'
playImplementation "com.google.firebase:firebase-inappmessaging-ktx"
@ -215,30 +203,25 @@ dependencies {
playImplementation 'com.google.android.play:core-ktx:1.8.1'
playImplementation 'com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava'
hmsImplementation 'com.huawei.hms:hianalytics:5.2.0.300'
hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.5.0.300'
hmsImplementation 'com.huawei.hms:hianalytics:5.1.0.301'
hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.4.2.301'
releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker"
debugImplementation "com.github.ChuckerTeam.Chucker:library:$chucker"
debugImplementation "com.amitshekhar.android:debug-db:1.0.6"
testImplementation "junit:junit:4.13.2"
testImplementation "junit:junit:4.13.1"
testImplementation "io.mockk:mockk:$mockk"
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.4.2'
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:runner:1.3.0"
androidTestImplementation "androidx.test.ext:junit:1.1.2"
androidTestImplementation "io.mockk:mockk-android:$mockk"
androidTestImplementation "androidx.room:room-testing:$room"
androidTestImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
}
apply plugin: 'com.google.gms.google-services'
apply plugin: 'com.huawei.agconnect'

View file

@ -1,21 +1,33 @@
# General
# Optimizations
-optimizationpasses 5
-optimizations !code/simplification/arithmetic,!field/*,!class/merging/*
-dontusemixedcaseclassnames
-dontskipnonpubliclibraryclasses
-dontskipnonpubliclibraryclassmembers
-dontpreverify
-dontobfuscate
-allowaccessmodification
-repackageclasses ''
-verbose
#Config for wulkanowy
#Keep all wulkanowy files
-keep class io.github.wulkanowy.** {*;}
#Config for firebase crashlitycs
#Config for anallitycs
-keepattributes *Annotation*
-keepattributes SourceFile,LineNumberTable
-keep class com.crashlytics.** {*;}
-keep public class * extends java.lang.Exception
-dontwarn com.crashlytics.**
#Config for Okio and OkHttp
-dontwarn javax.annotation.**
#Config for OkHttp
-keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase
-dontwarn org.codehaus.mojo.animal_sniffer.*
-dontwarn okhttp3.internal.platform.ConscryptPlatform
-dontwarn javax.annotation.**
#Config for MPAndroidChart
@ -24,3 +36,10 @@
#Config for Material Components
-keep class com.google.android.material.tabs.** { *; }
#Config for About Libraries
-keep class .R
-keep class **.R$* {
<fields>;
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,11 @@
package io.github.wulkanowy.data
import io.github.wulkanowy.utils.DispatchersProvider
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
class TestDispatchersProvider : DispatchersProvider() {
override val backgroundThread: CoroutineDispatcher
get() = Dispatchers.Unconfined
}

View file

@ -9,7 +9,6 @@ import androidx.test.core.app.ApplicationProvider
import androidx.test.platform.app.InstrumentationRegistry
import io.github.wulkanowy.data.db.AppDatabase
import io.github.wulkanowy.data.db.SharedPrefProvider
import io.github.wulkanowy.utils.AppInfo
import org.junit.Rule
abstract class AbstractMigrationTest {
@ -25,16 +24,12 @@ abstract class AbstractMigrationTest {
fun getMigratedRoomDatabase(): AppDatabase {
val context = ApplicationProvider.getApplicationContext<Context>()
val database = Room.databaseBuilder(
ApplicationProvider.getApplicationContext(),
AppDatabase::class.java,
dbName
).addMigrations(
*AppDatabase.getMigrations(
SharedPrefProvider(PreferenceManager.getDefaultSharedPreferences(context)),
AppInfo()
val database = Room.databaseBuilder(ApplicationProvider.getApplicationContext(),
AppDatabase::class.java, dbName)
.addMigrations(*AppDatabase.getMigrations(SharedPrefProvider(PreferenceManager
.getDefaultSharedPreferences(context)))
)
).build()
.build()
// close the database and release any stream resources when the test finishes
helper.closeWhenFinished(database)
return database

View file

@ -2,20 +2,14 @@ package io.github.wulkanowy.data.db.migrations
import android.content.ContentValues
import android.database.sqlite.SQLiteDatabase.CONFLICT_FAIL
import android.os.Build
import androidx.sqlite.db.SupportSQLiteDatabase
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.HiltTestApplication
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.runBlocking
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import kotlin.test.assertEquals
@HiltAndroidTest
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [Build.VERSION_CODES.O_MR1], application = HiltTestApplication::class)
@RunWith(AndroidJUnit4::class)
class Migration12Test : AbstractMigrationTest() {
@Test

View file

@ -2,26 +2,17 @@ package io.github.wulkanowy.data.db.migrations
import android.content.ContentValues
import android.database.sqlite.SQLiteDatabase
import android.os.Build
import androidx.sqlite.db.SupportSQLiteDatabase
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.HiltTestApplication
import io.github.wulkanowy.data.db.Converters
import io.github.wulkanowy.data.db.entities.Semester
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import java.time.LocalDate.of
import kotlin.test.assertFalse
import kotlin.test.assertTrue
@HiltAndroidTest
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [Build.VERSION_CODES.O_MR1], application = HiltTestApplication::class)
class Migration13Test : AbstractMigrationTest() {
@Test

View file

@ -2,21 +2,12 @@ package io.github.wulkanowy.data.db.migrations
import android.content.ContentValues
import android.database.sqlite.SQLiteDatabase
import android.os.Build
import androidx.sqlite.db.SupportSQLiteDatabase
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.HiltTestApplication
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import kotlin.random.Random
@HiltAndroidTest
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [Build.VERSION_CODES.O_MR1], application = HiltTestApplication::class)
class Migration27Test : AbstractMigrationTest() {
@Test

View file

@ -1,19 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#FFFFFF"
android:viewportWidth="24"
android:viewportHeight="24">
<group
android:scaleX="0.92"
android:scaleY="0.92"
android:translateX="0.96"
android:translateY="0.96">
<path
android:fillColor="#FFF"
android:pathData="M3.9512,2A2,2 0,0 0,2 4L2,18A2,2 0,0 0,4 20L10.0996,20C11.3596,21.24 13.09,22 15,22A7,7 0,0 0,15.7988 21.9551L15.7988,19.7832A4.85,4.85 0,0 1,15 19.8496C12.32,19.8496 10.1504,17.68 10.1504,15A4.85,4.85 0,0 1,15 10.1504C17.4677,10.1504 19.4978,11.9912 19.8047,14.375C20.566,14.3758 21.3108,14.5325 21.9922,14.834C21.9491,12.9905 21.2036,11.3226 20,10.0996L20,4A2,2 0,0 0,18 2L4,2A2,2 0,0 0,3.9512 2zM4,5L10,5L10,8L4,8L4,5zM12,5L18,5L18,8L12,8L12,5zM4,10L10.0996,10C9.2596,10.82 8.6291,11.85 8.2891,13L4,13L4,10zM14,12L14,15.6895L15.7988,16.7266L15.7988,14.9922L15.5,14.8203L15.5,12L14,12zM4,15L8,15C8,16.07 8.2399,17.09 8.6699,18L4,18L4,15z" />
<path
android:fillColor="#FFF"
android:pathData="m17.298,24v-8.1249h2.5c0.7143,0 1.3523,0.1618 1.9141,0.4855 0.5655,0.3199 1.0063,0.7775 1.3225,1.3728 0.3162,0.5915 0.4743,1.2649 0.4743,2.0201v0.3739c0,0.7552 -0.1562,1.4267 -0.4687,2.0145 -0.3088,0.5878 -0.7459,1.0435 -1.3114,1.3672C21.1633,23.8326 20.5253,23.9963 19.8148,24ZM18.9721,17.2311v5.4241h0.8091c0.6548,0 1.1551,-0.2139 1.5011,-0.6417 0.346,-0.4278 0.5227,-1.0398 0.5301,-1.8359v-0.4297c0,-0.8259 -0.1711,-1.4509 -0.5134,-1.875 -0.3423,-0.4278 -0.8426,-0.6417 -1.5011,-0.6417z" />
</group>
</vector>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 426 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 335 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 519 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 700 B

View file

@ -18,18 +18,6 @@
<action android:name="android.intent.action.VIEW" />
<data android:scheme="https" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="mailto" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="tel" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="geo" />
</intent>
</queries>
<application
@ -56,7 +44,7 @@
android:name=".ui.modules.login.LoginActivity"
android:configChanges="orientation|screenSize"
android:label="@string/login_title"
android:theme="@style/WulkanowyTheme.Login"
android:theme="@style/WulkanowyTheme.NoActionBar"
android:windowSoftInputMode="adjustResize" />
<activity
android:name=".ui.modules.main.MainActivity"
@ -68,7 +56,7 @@
android:name=".ui.modules.message.send.SendMessageActivity"
android:configChanges="orientation|screenSize"
android:label="@string/send_message_title"
android:theme="@style/WulkanowyTheme.MessageSend"
android:theme="@style/WulkanowyTheme.NoActionBar"
android:windowSoftInputMode="adjustResize" />
<activity
android:name=".ui.modules.timetablewidget.TimetableWidgetConfigureActivity"

View file

@ -34,17 +34,5 @@
{
"displayName": "Mateusz Idziejczak",
"githubUsername": "Luncenok"
},
{
"displayName": "MRmlik12",
"githubUsername": "MRmlik12"
},
{
"displayName": "Damian Czupryn",
"githubUsername": "Daxxxis"
},
{
"displayName": "Kamil Studziński",
"githubUsername": "studzinskik"
}
]

View file

@ -1,13 +1,10 @@
package io.github.wulkanowy
import android.annotation.SuppressLint
import android.app.Application
import android.content.Context
import android.util.Log.DEBUG
import android.util.Log.INFO
import android.util.Log.VERBOSE
import android.webkit.WebView
import androidx.fragment.app.FragmentManager
import androidx.hilt.work.HiltWorkerFactory
import androidx.multidex.MultiDex
import androidx.work.Configuration
@ -48,26 +45,24 @@ class WulkanowyApp : Application(), Configuration.Provider {
MultiDex.install(this)
}
@SuppressLint("UnsafeOptInUsageWarning")
override fun onCreate() {
super.onCreate()
FragmentManager.enableNewStateManager(false)
initializeAppLanguage()
Lingver.init(this)
themeManager.applyDefaultTheme()
initLogging()
fixWebViewLocale()
logCurrentLanguage()
}
private fun initLogging() {
if (appInfo.isDebug) {
Timber.plant(DebugLogTree())
Timber.plant(
FileLoggerTree.Builder()
.withFileName("wulkanowy.%g.log")
.withDirName(applicationContext.filesDir.absolutePath)
.withFileLimit(10)
.withMinPriority(DEBUG)
.build()
Timber.plant(FileLoggerTree.Builder()
.withFileName("wulkanowy.%g.log")
.withDirName(applicationContext.filesDir.absolutePath)
.withFileLimit(10)
.withMinPriority(DEBUG)
.build()
)
} else {
Timber.plant(CrashLogExceptionTree())
@ -76,24 +71,14 @@ class WulkanowyApp : Application(), Configuration.Provider {
registerActivityLifecycleCallbacks(ActivityLifecycleLogger())
}
private fun initializeAppLanguage() {
Lingver.init(this)
if (preferencesRepository.appLanguage == "system") {
Lingver.getInstance().setFollowSystemLocale(this)
analyticsHelper.logEvent("language", "startup" to appInfo.systemLanguage)
private fun logCurrentLanguage() {
val newLang = if (preferencesRepository.appLanguage == "system") {
appInfo.systemLanguage
} else {
analyticsHelper.logEvent("language", "startup" to preferencesRepository.appLanguage)
preferencesRepository.appLanguage
}
}
private fun fixWebViewLocale() {
//https://stackoverflow.com/questions/40398528/android-webview-language-changes-abruptly-on-android-7-0-and-above
try {
WebView(this).destroy()
} catch (e: Exception) {
//Ignore exceptions
}
analyticsHelper.logEvent("language", "startup" to newLang)
}
override fun getWorkManagerConfiguration() = Configuration.Builder()

View file

@ -17,7 +17,6 @@ import io.github.wulkanowy.data.db.AppDatabase
import io.github.wulkanowy.data.db.SharedPrefProvider
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.AppInfo
import timber.log.Timber
import javax.inject.Singleton
@ -34,21 +33,17 @@ internal class RepositoryModule {
setSimpleHttpLogger { Timber.d(it) }
// for debug only
addInterceptor(
ChuckerInterceptor.Builder(context)
.collector(chuckerCollector)
.alwaysReadResponseBody(true)
.build(), network = true
addInterceptor(ChuckerInterceptor.Builder(context)
.collector(chuckerCollector)
.alwaysReadResponseBody(true)
.build(), network = true
)
}
}
@Singleton
@Provides
fun provideChuckerCollector(
@ApplicationContext context: Context,
prefRepository: PreferencesRepository
): ChuckerCollector {
fun provideChuckerCollector(@ApplicationContext context: Context, prefRepository: PreferencesRepository): ChuckerCollector {
return ChuckerCollector(
context = context,
showNotification = prefRepository.isDebugNotificationEnable,
@ -58,11 +53,7 @@ internal class RepositoryModule {
@Singleton
@Provides
fun provideDatabase(
@ApplicationContext context: Context,
sharedPrefProvider: SharedPrefProvider,
appInfo: AppInfo
) = AppDatabase.newInstance(context, sharedPrefProvider, appInfo)
fun provideDatabase(@ApplicationContext context: Context, sharedPrefProvider: SharedPrefProvider) = AppDatabase.newInstance(context, sharedPrefProvider)
@Singleton
@Provides
@ -74,8 +65,7 @@ internal class RepositoryModule {
@Singleton
@Provides
fun provideSharedPref(@ApplicationContext context: Context): SharedPreferences =
PreferenceManager.getDefaultSharedPreferences(context)
fun provideSharedPref(@ApplicationContext context: Context): SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
@Singleton
@Provides
@ -99,8 +89,7 @@ internal class RepositoryModule {
@Singleton
@Provides
fun provideGradeSemesterStatisticsDao(database: AppDatabase) =
database.gradeSemesterStatisticsDao
fun provideGradeSemesterStatisticsDao(database: AppDatabase) = database.gradeSemesterStatisticsDao
@Singleton
@Provides
@ -177,8 +166,4 @@ internal class RepositoryModule {
@Singleton
@Provides
fun provideTimetableAdditionalDao(database: AppDatabase) = database.timetableAdditionalDao
@Singleton
@Provides
fun provideStudentInfoDao(database: AppDatabase) = database.studentInfoDao
}

View file

@ -6,6 +6,7 @@ import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.RoomDatabase.JournalMode.TRUNCATE
import androidx.room.TypeConverters
import androidx.room.migration.Migration
import io.github.wulkanowy.data.db.dao.AttendanceDao
import io.github.wulkanowy.data.db.dao.AttendanceSummaryDao
import io.github.wulkanowy.data.db.dao.CompletedLessonsDao
@ -27,7 +28,6 @@ import io.github.wulkanowy.data.db.dao.ReportingUnitDao
import io.github.wulkanowy.data.db.dao.SchoolDao
import io.github.wulkanowy.data.db.dao.SemesterDao
import io.github.wulkanowy.data.db.dao.StudentDao
import io.github.wulkanowy.data.db.dao.StudentInfoDao
import io.github.wulkanowy.data.db.dao.SubjectDao
import io.github.wulkanowy.data.db.dao.TeacherDao
import io.github.wulkanowy.data.db.dao.TimetableAdditionalDao
@ -53,7 +53,6 @@ import io.github.wulkanowy.data.db.entities.ReportingUnit
import io.github.wulkanowy.data.db.entities.School
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentInfo
import io.github.wulkanowy.data.db.entities.Subject
import io.github.wulkanowy.data.db.entities.Teacher
import io.github.wulkanowy.data.db.entities.Timetable
@ -81,18 +80,12 @@ import io.github.wulkanowy.data.db.migrations.Migration28
import io.github.wulkanowy.data.db.migrations.Migration29
import io.github.wulkanowy.data.db.migrations.Migration3
import io.github.wulkanowy.data.db.migrations.Migration30
import io.github.wulkanowy.data.db.migrations.Migration31
import io.github.wulkanowy.data.db.migrations.Migration32
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.Migration5
import io.github.wulkanowy.data.db.migrations.Migration6
import io.github.wulkanowy.data.db.migrations.Migration7
import io.github.wulkanowy.data.db.migrations.Migration8
import io.github.wulkanowy.data.db.migrations.Migration9
import io.github.wulkanowy.utils.AppInfo
import javax.inject.Singleton
@Singleton
@ -123,7 +116,6 @@ import javax.inject.Singleton
School::class,
Conference::class,
TimetableAdditional::class,
StudentInfo::class,
],
version = AppDatabase.VERSION_SCHEMA,
exportSchema = true
@ -132,55 +124,50 @@ import javax.inject.Singleton
abstract class AppDatabase : RoomDatabase() {
companion object {
const val VERSION_SCHEMA = 35
const val VERSION_SCHEMA = 30
fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf(
Migration2(),
Migration3(),
Migration4(),
Migration5(),
Migration6(),
Migration7(),
Migration8(),
Migration9(),
Migration10(),
Migration11(),
Migration12(),
Migration13(),
Migration14(),
Migration15(),
Migration16(),
Migration17(),
Migration18(),
Migration19(sharedPrefProvider),
Migration20(),
Migration21(),
Migration22(),
Migration23(),
Migration24(),
Migration25(),
Migration26(),
Migration27(),
Migration28(),
Migration29(),
Migration30(),
Migration31(),
Migration32(),
Migration33(),
Migration34(),
Migration35(appInfo)
)
fun getMigrations(sharedPrefProvider: SharedPrefProvider): Array<Migration> {
return arrayOf(
Migration2(),
Migration3(),
Migration4(),
Migration5(),
Migration6(),
Migration7(),
Migration8(),
Migration9(),
Migration10(),
Migration11(),
Migration12(),
Migration13(),
Migration14(),
Migration15(),
Migration16(),
Migration17(),
Migration18(),
Migration19(sharedPrefProvider),
Migration20(),
Migration21(),
Migration22(),
Migration23(),
Migration24(),
Migration25(),
Migration26(),
Migration27(),
Migration28(),
Migration29(),
Migration30(),
)
}
fun newInstance(
context: Context,
sharedPrefProvider: SharedPrefProvider,
appInfo: AppInfo
) = Room.databaseBuilder(context, AppDatabase::class.java, "wulkanowy_database")
.setJournalMode(TRUNCATE)
.fallbackToDestructiveMigrationFrom(VERSION_SCHEMA + 1)
.fallbackToDestructiveMigrationOnDowngrade()
.addMigrations(*getMigrations(sharedPrefProvider, appInfo))
.build()
fun newInstance(context: Context, sharedPrefProvider: SharedPrefProvider): AppDatabase {
return Room.databaseBuilder(context, AppDatabase::class.java, "wulkanowy_database")
.setJournalMode(TRUNCATE)
.fallbackToDestructiveMigrationFrom(VERSION_SCHEMA + 1)
.fallbackToDestructiveMigrationOnDowngrade()
.addMigrations(*getMigrations(sharedPrefProvider))
.build()
}
}
abstract val studentDao: StudentDao
@ -232,6 +219,4 @@ abstract class AppDatabase : RoomDatabase() {
abstract val conferenceDao: ConferenceDao
abstract val timetableAdditionalDao: TimetableAdditionalDao
abstract val studentInfoDao: StudentInfoDao
}

View file

@ -13,7 +13,4 @@ interface LuckyNumberDao : BaseDao<LuckyNumber> {
@Query("SELECT * FROM LuckyNumbers WHERE student_id = :studentId AND date = :date")
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

@ -6,9 +6,7 @@ import androidx.room.Insert
import androidx.room.OnConflictStrategy.ABORT
import androidx.room.Query
import androidx.room.Transaction
import androidx.room.Update
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentNickAndAvatar
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import javax.inject.Singleton
@ -22,14 +20,11 @@ interface StudentDao {
@Delete
suspend fun delete(student: Student)
@Update(entity = Student::class)
suspend fun update(studentNickAndAvatar: StudentNickAndAvatar)
@Query("SELECT * FROM Students WHERE is_current = 1")
suspend fun loadCurrent(): Student?
@Query("SELECT * FROM Students WHERE id = :id")
suspend fun loadById(id: Long): Student?
suspend fun loadById(id: Int): Student?
@Query("SELECT * FROM Students")
suspend fun loadAll(): List<Student>

View file

@ -1,15 +0,0 @@
package io.github.wulkanowy.data.db.dao
import androidx.room.Dao
import androidx.room.Query
import io.github.wulkanowy.data.db.entities.StudentInfo
import kotlinx.coroutines.flow.Flow
import javax.inject.Singleton
@Singleton
@Dao
interface StudentInfoDao : BaseDao<StudentInfo> {
@Query("SELECT * FROM StudentInfo WHERE student_id = :studentId")
fun loadStudentInfo(studentId: Int): Flow<StudentInfo?>
}

View file

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

View file

@ -7,13 +7,7 @@ import androidx.room.PrimaryKey
import java.io.Serializable
import java.time.LocalDateTime
@Entity(
tableName = "Students",
indices = [Index(
value = ["email", "symbol", "student_id", "school_id", "class_id"],
unique = true
)]
)
@Entity(tableName = "Students", indices = [Index(value = ["email", "symbol", "student_id", "school_id", "class_id"], unique = true)])
data class Student(
@ColumnInfo(name = "scrapper_base_url")
@ -58,7 +52,7 @@ data class Student(
@ColumnInfo(name = "school_id")
val schoolSymbol: String,
@ColumnInfo(name = "school_short")
@ColumnInfo(name ="school_short")
val schoolShortName: String,
@ColumnInfo(name = "school_name")
@ -79,9 +73,4 @@ data class Student(
@PrimaryKey(autoGenerate = true)
var id: Long = 0
var nick = ""
@ColumnInfo(name = "avatar_color")
var avatarColor = 0L
}

View file

@ -1,85 +0,0 @@
package io.github.wulkanowy.data.db.entities
import androidx.room.ColumnInfo
import androidx.room.Embedded
import androidx.room.Entity
import androidx.room.PrimaryKey
import io.github.wulkanowy.data.enums.Gender
import java.io.Serializable
import java.time.LocalDate
@Entity(tableName = "StudentInfo")
data class StudentInfo(
@ColumnInfo(name = "student_id")
val studentId: Int,
@ColumnInfo(name = "full_name")
val fullName: String,
@ColumnInfo(name = "first_name")
val firstName: String,
@ColumnInfo(name = "second_name")
val secondName: String,
val surname: String,
@ColumnInfo(name = "birth_date")
val birthDate: LocalDate,
@ColumnInfo(name = "birth_place")
val birthPlace: String,
val gender: Gender,
@ColumnInfo(name = "has_polish_citizenship")
val hasPolishCitizenship: Boolean,
@ColumnInfo(name = "family_name")
val familyName: String,
@ColumnInfo(name = "parents_names")
val parentsNames: String,
val address: String,
@ColumnInfo(name = "registered_address")
val registeredAddress: String,
@ColumnInfo(name = "correspondence_address")
val correspondenceAddress: String,
@ColumnInfo(name = "phone_number")
val phoneNumber: String,
@ColumnInfo(name = "cell_phone_number")
val cellPhoneNumber: String,
val email: String,
@Embedded(prefix = "first_guardian_")
val firstGuardian: StudentGuardian?,
@Embedded(prefix = "second_guardian_")
val secondGuardian: StudentGuardian?
) : Serializable {
@PrimaryKey(autoGenerate = true)
var id: Long = 0
}
data class StudentGuardian(
@ColumnInfo(name = "full_name")
val fullName: String,
val kinship: String,
val address: String,
val phones: String,
val email: String
) : Serializable

View file

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

View file

@ -1,42 +0,0 @@
package io.github.wulkanowy.data.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration31 : Migration(30, 31) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
"""CREATE TABLE IF NOT EXISTS StudentInfo (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
student_id INTEGER NOT NULL,
full_name TEXT NOT NULL,
first_name TEXT NOT NULL,
second_name TEXT NOT NULL,
surname TEXT NOT NULL,
birth_date INTEGER NOT NULL,
birth_place TEXT NOT NULL,
gender TEXT NOT NULL,
has_polish_citizenship INTEGER NOT NULL,
family_name TEXT NOT NULL,
parents_names TEXT NOT NULL,
address TEXT NOT NULL,
registered_address TEXT NOT NULL,
correspondence_address TEXT NOT NULL,
phone_number TEXT NOT NULL,
cell_phone_number TEXT NOT NULL,
email TEXT NOT NULL,
first_guardian_full_name TEXT NOT NULL,
first_guardian_kinship TEXT NOT NULL,
first_guardian_address TEXT NOT NULL,
first_guardian_phones TEXT NOT NULL,
first_guardian_email TEXT NOT NULL,
second_guardian_full_name TEXT NOT NULL,
second_guardian_kinship TEXT NOT NULL,
second_guardian_address TEXT NOT NULL,
second_guardian_phones TEXT NOT NULL,
second_guardian_email TEXT NOT NULL)
"""
)
}
}

View file

@ -1,12 +0,0 @@
package io.github.wulkanowy.data.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration32 : Migration(31, 32) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE Students ADD COLUMN nick TEXT NOT NULL DEFAULT \"\"")
}
}

View file

@ -1,45 +0,0 @@
package io.github.wulkanowy.data.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration33 : Migration(32, 33) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("DROP TABLE IF EXISTS StudentInfo")
database.execSQL(
"""CREATE TABLE IF NOT EXISTS StudentInfo (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
student_id INTEGER NOT NULL,
full_name TEXT NOT NULL,
first_name TEXT NOT NULL,
second_name TEXT NOT NULL,
surname TEXT NOT NULL,
birth_date INTEGER NOT NULL,
birth_place TEXT NOT NULL,
gender TEXT NOT NULL,
has_polish_citizenship INTEGER NOT NULL,
family_name TEXT NOT NULL,
parents_names TEXT NOT NULL,
address TEXT NOT NULL,
registered_address TEXT NOT NULL,
correspondence_address TEXT NOT NULL,
phone_number TEXT NOT NULL,
cell_phone_number TEXT NOT NULL,
email TEXT NOT NULL,
first_guardian_full_name TEXT,
first_guardian_kinship TEXT,
first_guardian_address TEXT,
first_guardian_phones TEXT,
first_guardian_email TEXT,
second_guardian_full_name TEXT,
second_guardian_kinship TEXT,
second_guardian_address TEXT,
second_guardian_phones TEXT,
second_guardian_email TEXT)
"""
)
}
}

View file

@ -1,13 +0,0 @@
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

@ -1,24 +0,0 @@
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

@ -1,3 +0,0 @@
package io.github.wulkanowy.data.enums
enum class Gender { MALE, FEMALE }

View file

@ -5,9 +5,10 @@ import io.github.wulkanowy.data.db.entities.GradePointsStatistics
import io.github.wulkanowy.data.db.entities.GradeSemesterStatistics
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.pojos.GradeStatisticsItem
import io.github.wulkanowy.sdk.pojo.GradePointsStatistics as SdkGradePointsStatistics
import io.github.wulkanowy.sdk.pojo.GradeStatisticsSemester as SdkGradeStatisticsSemester
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
@JvmName("mapToEntitiesSubject")
fun List<SdkGradeStatisticsSubject>.mapToEntities(semester: Semester) = map {
@ -50,7 +51,7 @@ fun List<SdkGradePointsStatistics>.mapToEntities(semester: Semester) = map {
fun List<GradePartialStatistics>.mapPartialToStatisticItems() = filterNot { it.classAmounts.isEmpty() }.map {
GradeStatisticsItem(
type = GradeStatisticsItem.DataType.PARTIAL,
type = ViewType.PARTIAL,
average = it.classAverage,
partial = it,
points = null,
@ -60,7 +61,7 @@ fun List<GradePartialStatistics>.mapPartialToStatisticItems() = filterNot { it.c
fun List<GradeSemesterStatistics>.mapSemesterToStatisticItems() = filterNot { it.amounts.isEmpty() }.map {
GradeStatisticsItem(
type = GradeStatisticsItem.DataType.SEMESTER,
type = ViewType.SEMESTER,
partial = null,
points = null,
average = "",
@ -70,7 +71,7 @@ fun List<GradeSemesterStatistics>.mapSemesterToStatisticItems() = filterNot { it
fun List<GradePointsStatistics>.mapPointsToStatisticsItems() = map {
GradeStatisticsItem(
type = GradeStatisticsItem.DataType.POINTS,
type = ViewType.POINTS,
partial = null,
semester = null,
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.Recipient
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 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 {
Message(
studentId = student.id,
studentId = student.id.toInt(),
realId = it.id ?: 0,
messageId = it.messageId ?: 0,
sender = it.sender?.name.orEmpty(),

View file

@ -1,38 +0,0 @@
package io.github.wulkanowy.data.mappers
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.StudentGuardian
import io.github.wulkanowy.data.db.entities.StudentInfo
import io.github.wulkanowy.data.enums.Gender
import io.github.wulkanowy.sdk.pojo.StudentGuardian as SdkStudentGuardian
import io.github.wulkanowy.sdk.pojo.StudentInfo as SdkStudentInfo
fun SdkStudentInfo.mapToEntity(semester: Semester) = StudentInfo(
studentId = semester.studentId,
fullName = fullName,
firstName = firstName,
secondName = secondName,
surname = surname,
birthDate = birthDate,
birthPlace = birthPlace,
gender = Gender.valueOf(gender.name),
hasPolishCitizenship = hasPolishCitizenship,
familyName = familyName,
parentsNames = parentsNames,
address = address,
registeredAddress = registeredAddress,
correspondenceAddress = correspondenceAddress,
phoneNumber = phoneNumber,
cellPhoneNumber = phoneNumber,
email = email,
firstGuardian = guardianFirst?.mapToEntity(),
secondGuardian = guardianSecond?.mapToEntity()
)
fun SdkStudentGuardian.mapToEntity() = StudentGuardian(
fullName = fullName,
kinship = kinship,
address = address,
phones = phones,
email = email
)

View file

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

View file

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

View file

@ -14,7 +14,6 @@ import io.github.wulkanowy.utils.monday
import io.github.wulkanowy.utils.networkBoundResource
import io.github.wulkanowy.utils.sunday
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.sync.Mutex
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime
@ -28,12 +27,9 @@ class AttendanceRepository @Inject constructor(
private val refreshHelper: AutoRefreshHelper,
) {
private val saveFetchResultMutex = Mutex()
private val cacheKey = "attendance"
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)) },
query = { attendanceDb.loadAll(semester.diaryId, semester.studentId, start.monday, end.sunday) },
fetch = {

View file

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

View file

@ -12,7 +12,6 @@ import io.github.wulkanowy.utils.monday
import io.github.wulkanowy.utils.networkBoundResource
import io.github.wulkanowy.utils.sunday
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.sync.Mutex
import java.time.LocalDate
import javax.inject.Inject
import javax.inject.Singleton
@ -24,12 +23,9 @@ class CompletedLessonsRepository @Inject constructor(
private val refreshHelper: AutoRefreshHelper,
) {
private val saveFetchResultMutex = Mutex()
private val cacheKey = "completed"
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)) },
query = { completedLessonsDb.loadAll(semester.studentId, semester.diaryId, start.monday, end.sunday) },
fetch = {

View file

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

View file

@ -12,7 +12,6 @@ import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.networkBoundResource
import io.github.wulkanowy.utils.startExamsDay
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.sync.Mutex
import java.time.LocalDate
import javax.inject.Inject
import javax.inject.Singleton
@ -24,12 +23,9 @@ class ExamRepository @Inject constructor(
private val refreshHelper: AutoRefreshHelper,
) {
private val saveFetchResultMutex = Mutex()
private val cacheKey = "exam"
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)) },
query = { examDb.loadAll(semester.diaryId, semester.studentId, start.startExamsDay, start.endExamsDay) },
fetch = {

View file

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

View file

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

View file

@ -13,7 +13,6 @@ import io.github.wulkanowy.utils.monday
import io.github.wulkanowy.utils.networkBoundResource
import io.github.wulkanowy.utils.sunday
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.sync.Mutex
import java.time.LocalDate
import javax.inject.Inject
import javax.inject.Singleton
@ -25,12 +24,9 @@ class HomeworkRepository @Inject constructor(
private val refreshHelper: AutoRefreshHelper,
) {
private val saveFetchResultMutex = Mutex()
private val cacheKey = "homework"
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)) },
query = { homeworkDb.loadAll(semester.semesterId, semester.studentId, start.monday, end.sunday) },
fetch = {

View file

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

View file

@ -20,7 +20,6 @@ import io.github.wulkanowy.utils.networkBoundResource
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.sync.Mutex
import timber.log.Timber
import java.time.LocalDateTime.now
import javax.inject.Inject
@ -34,13 +33,10 @@ class MessageRepository @Inject constructor(
private val refreshHelper: AutoRefreshHelper,
) {
private val saveFetchResultMutex = Mutex()
private val cacheKey = "message"
@Suppress("UNUSED_PARAMETER")
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)) },
query = { messagesDb.loadAll(student.id.toInt(), folder.id) },
fetch = { sdk.init(student).getMessages(Folder.valueOf(folder.name), now().minusMonths(3), now()).mapToEntities(student) },

View file

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

View file

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

View file

@ -18,43 +18,26 @@ class PreferencesRepository @Inject constructor(
get() = getString(R.string.pref_key_start_menu, R.string.pref_default_startup).toInt()
val isShowPresent: Boolean
get() = getBoolean(
R.string.pref_key_attendance_present,
R.bool.pref_default_attendance_present
)
get() = getBoolean(R.string.pref_key_attendance_present, R.bool.pref_default_attendance_present)
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
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
get() = !getBoolean(R.string.pref_key_expand_grade, R.bool.pref_default_expand_grade)
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 appTheme: String
get() = getString(appThemeKey, R.string.pref_default_app_theme)
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 appLanguage
@ -72,86 +55,50 @@ class PreferencesRepository @Inject constructor(
val isServicesOnlyWifi: Boolean
get() = getBoolean(servicesOnlyWifiKey, R.bool.pref_default_services_wifi_only)
val notificationsEnableKey = context.getString(R.string.pref_key_notifications_enable)
val isNotificationsEnable: Boolean
get() = getBoolean(notificationsEnableKey, R.bool.pref_default_notifications_enable)
get() = getBoolean(R.string.pref_key_notifications_enable, 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
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 isDebugNotificationEnable: Boolean
get() = getBoolean(isDebugNotificationEnableKey, R.bool.pref_default_notification_debug)
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
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
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
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
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
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
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
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()
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: 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: 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,7 +7,6 @@ import io.github.wulkanowy.data.mappers.mapToEntity
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.networkBoundResource
import kotlinx.coroutines.sync.Mutex
import javax.inject.Inject
import javax.inject.Singleton
@ -17,26 +16,16 @@ class SchoolRepository @Inject constructor(
private val sdk: Sdk
) {
private val saveFetchResultMutex = Mutex()
fun getSchoolInfo(student: Student, semester: Semester, forceRefresh: Boolean) =
networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = { it == null || forceRefresh },
query = { schoolDb.load(semester.studentId, semester.classId) },
fetch = {
sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear).getSchool()
.mapToEntity(semester)
},
saveFetchResult = { old, new ->
if (old != null && new != old) {
with(schoolDb) {
deleteAll(listOf(old))
insertAll(listOf(new))
}
} else if (old == null) {
schoolDb.insertAll(listOf(new))
}
fun getSchoolInfo(student: Student, semester: Semester, forceRefresh: Boolean) = networkBoundResource(
shouldFetch = { it == null || forceRefresh },
query = { schoolDb.load(semester.studentId, semester.classId) },
fetch = { sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear).getSchool().mapToEntity(semester) },
saveFetchResult = { old, new ->
if (new != old && old != null) {
schoolDb.deleteAll(listOf(old))
schoolDb.insertAll(listOf(new))
}
)
schoolDb.insertAll(listOf(new))
}
)
}

View file

@ -1,42 +0,0 @@
package io.github.wulkanowy.data.repositories
import io.github.wulkanowy.data.db.dao.StudentInfoDao
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.mappers.mapToEntity
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.networkBoundResource
import kotlinx.coroutines.sync.Mutex
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class StudentInfoRepository @Inject constructor(
private val studentInfoDao: StudentInfoDao,
private val sdk: Sdk
) {
private val saveFetchResultMutex = Mutex()
fun getStudentInfo(student: Student, semester: Semester, forceRefresh: Boolean) =
networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = { it == null || forceRefresh },
query = { studentInfoDao.loadStudentInfo(student.studentId) },
fetch = {
sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear)
.getStudentInfo().mapToEntity(semester)
},
saveFetchResult = { old, new ->
if (old != null && new != old) {
with(studentInfoDao) {
deleteAll(listOf(old))
insertAll(listOf(new))
}
} else if (old == null) {
studentInfoDao.insertAll(listOf(new))
}
}
)
}

View file

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

View file

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

View file

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

View file

@ -18,7 +18,6 @@ import io.github.wulkanowy.utils.sunday
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.sync.Mutex
import java.time.LocalDate
import javax.inject.Inject
import javax.inject.Singleton
@ -32,12 +31,9 @@ class TimetableRepository @Inject constructor(
private val refreshHelper: AutoRefreshHelper,
) {
private val saveFetchResultMutex = Mutex()
private val cacheKey = "timetable"
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)) },
query = {
timetableDb.loadAll(semester.diaryId, semester.studentId, start.monday, end.sunday)

View file

@ -27,7 +27,6 @@ import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companio
import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companion.STUDENT_NAME
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.DispatchersProvider
import io.github.wulkanowy.utils.nickOrName
import io.github.wulkanowy.utils.toTimestamp
import kotlinx.coroutines.withContext
import timber.log.Timber
@ -42,23 +41,17 @@ class TimetableNotificationSchedulerHelper @Inject constructor(
private val dispatchersProvider: DispatchersProvider,
) {
private fun getRequestCode(time: LocalDateTime, studentId: Int) =
(time.toTimestamp() * studentId).toInt()
private fun getRequestCode(time: LocalDateTime, studentId: Int) = (time.toTimestamp() * studentId).toInt()
private fun getUpcomingLessonTime(
index: Int,
day: List<Timetable>,
lesson: Timetable
) = day.getOrNull(index - 1)?.end ?: lesson.start.minusMinutes(30)
private fun getUpcomingLessonTime(index: Int, day: List<Timetable>, lesson: Timetable): LocalDateTime {
return day.getOrNull(index - 1)?.end ?: lesson.start.minusMinutes(30)
}
suspend fun cancelScheduled(lessons: List<Timetable>, studentId: Int = 1) {
withContext(dispatchersProvider.backgroundThread) {
lessons.sortedBy { it.start }.forEachIndexed { index, lesson ->
val upcomingTime = getUpcomingLessonTime(index, lessons, lesson)
cancelScheduledTo(
upcomingTime..lesson.start,
getRequestCode(upcomingTime, studentId)
)
cancelScheduledTo(upcomingTime..lesson.start, getRequestCode(upcomingTime, studentId))
cancelScheduledTo(lesson.start..lesson.end, getRequestCode(lesson.start, studentId))
Timber.d("TimetableNotification canceled: type 1 & 2, subject: ${lesson.subject}, start: ${lesson.start}, student: $studentId")
@ -68,18 +61,13 @@ class TimetableNotificationSchedulerHelper @Inject constructor(
private fun cancelScheduledTo(range: ClosedRange<LocalDateTime>, requestCode: Int) {
if (now() in range) cancelNotification()
alarmManager.cancel(
PendingIntent.getBroadcast(context, requestCode, Intent(), FLAG_UPDATE_CURRENT)
)
alarmManager.cancel(PendingIntent.getBroadcast(context, requestCode, Intent(), FLAG_UPDATE_CURRENT))
}
fun cancelNotification() =
NotificationManagerCompat.from(context).cancel(MainView.Section.TIMETABLE.id)
fun cancelNotification() = NotificationManagerCompat.from(context).cancel(MainView.Section.TIMETABLE.id)
suspend fun scheduleNotifications(lessons: List<Timetable>, student: Student) {
if (!preferencesRepository.isUpcomingLessonsNotificationsEnable) {
return cancelScheduled(lessons, student.studentId)
}
if (!preferencesRepository.isUpcomingLessonsNotificationsEnable) return cancelScheduled(lessons, student.studentId)
withContext(dispatchersProvider.backgroundThread) {
lessons.groupBy { it.date }
@ -94,28 +82,13 @@ class TimetableNotificationSchedulerHelper @Inject constructor(
val intent = createIntent(student, lesson, active.getOrNull(index + 1))
if (lesson.start > now()) {
scheduleBroadcast(
intent,
student.studentId,
NOTIFICATION_TYPE_UPCOMING,
getUpcomingLessonTime(index, active, lesson)
)
scheduleBroadcast(intent, student.studentId, NOTIFICATION_TYPE_UPCOMING, getUpcomingLessonTime(index, active, lesson))
}
if (lesson.end > now()) {
scheduleBroadcast(
intent,
student.studentId,
NOTIFICATION_TYPE_CURRENT,
lesson.start
)
scheduleBroadcast(intent, student.studentId, NOTIFICATION_TYPE_CURRENT, lesson.start)
if (active.lastIndex == index) {
scheduleBroadcast(
intent,
student.studentId,
NOTIFICATION_TYPE_LAST_LESSON_CANCELLATION,
lesson.end
)
scheduleBroadcast(intent, student.studentId, NOTIFICATION_TYPE_LAST_LESSON_CANCELLATION, lesson.end)
}
}
}
@ -126,7 +99,7 @@ class TimetableNotificationSchedulerHelper @Inject constructor(
private fun createIntent(student: Student, lesson: Timetable, nextLesson: Timetable?): Intent {
return Intent(context, TimetableNotificationReceiver::class.java).apply {
putExtra(STUDENT_ID, student.studentId)
putExtra(STUDENT_NAME, student.nickOrName)
putExtra(STUDENT_NAME, student.studentName)
putExtra(LESSON_ROOM, lesson.room)
putExtra(LESSON_START, lesson.start.toTimestamp())
putExtra(LESSON_END, lesson.end.toTimestamp())
@ -136,23 +109,13 @@ class TimetableNotificationSchedulerHelper @Inject constructor(
}
}
private fun scheduleBroadcast(
intent: Intent,
studentId: Int,
notificationType: Int,
time: LocalDateTime
) {
AlarmManagerCompat.setExactAndAllowWhileIdle(
alarmManager, RTC_WAKEUP, time.toTimestamp(),
private fun scheduleBroadcast(intent: Intent, studentId: Int, notificationType: Int, time: LocalDateTime) {
AlarmManagerCompat.setExactAndAllowWhileIdle(alarmManager, RTC_WAKEUP, time.toTimestamp(),
PendingIntent.getBroadcast(context, getRequestCode(time, studentId), intent.also {
it.putExtra(NOTIFICATION_ID, MainView.Section.TIMETABLE.id)
it.putExtra(LESSON_TYPE, notificationType)
}, FLAG_UPDATE_CURRENT)
)
Timber.d(
"TimetableNotification scheduled: type: $notificationType, subject: ${
intent.getStringExtra(LESSON_TITLE)
}, start: $time, student: $studentId"
)
Timber.d("TimetableNotification scheduled: type: $notificationType, subject: ${intent.getStringExtra(LESSON_TITLE)}, start: $time, student: $studentId")
}
}

View file

@ -5,12 +5,11 @@ import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationCompat.BigTextStyle
import androidx.core.app.NotificationCompat.PRIORITY_DEFAULT
import androidx.core.app.NotificationManagerCompat
import androidx.hilt.work.HiltWorker
import androidx.hilt.Assisted
import androidx.hilt.work.WorkerInject
import androidx.work.CoroutineWorker
import androidx.work.Data
import androidx.work.WorkerParameters
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.github.wulkanowy.R
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.SemesterRepository
@ -24,8 +23,7 @@ import kotlinx.coroutines.coroutineScope
import timber.log.Timber
import kotlin.random.Random
@HiltWorker
class SyncWorker @AssistedInject constructor(
class SyncWorker @WorkerInject constructor(
@Assisted appContext: Context,
@Assisted workerParameters: WorkerParameters,
private val studentRepository: StudentRepository,
@ -60,10 +58,9 @@ class SyncWorker @AssistedInject constructor(
}
val result = when {
exceptions.isNotEmpty() && inputData.getBoolean("one_time", false) -> {
Result.failure(
Data.Builder()
.putString("error", exceptions.map { it.stackTraceToString() }.toString())
.build()
Result.failure(Data.Builder()
.putString("error", exceptions.map { it.stackTraceToString() }.toString())
.build()
)
}
exceptions.isNotEmpty() -> Result.retry()
@ -77,16 +74,13 @@ class SyncWorker @AssistedInject constructor(
}
private fun notify(result: Result) {
notificationManager.notify(
Random.nextInt(Int.MAX_VALUE),
NotificationCompat.Builder(applicationContext, DebugChannel.CHANNEL_ID)
.setContentTitle("Debug notification")
.setSmallIcon(R.drawable.ic_stat_push)
.setAutoCancel(true)
.setColor(applicationContext.getCompatColor(R.color.colorPrimary))
.setStyle(BigTextStyle().bigText("${SyncWorker::class.java.simpleName} result: $result"))
.setPriority(PRIORITY_DEFAULT)
.build()
)
notificationManager.notify(Random.nextInt(Int.MAX_VALUE), NotificationCompat.Builder(applicationContext, DebugChannel.CHANNEL_ID)
.setContentTitle("Debug notification")
.setSmallIcon(R.drawable.ic_stat_push)
.setAutoCancel(true)
.setColor(applicationContext.getCompatColor(R.color.colorPrimary))
.setStyle(BigTextStyle().bigText("${SyncWorker::class.java.simpleName} result: $result"))
.setPriority(PRIORITY_DEFAULT)
.build())
}
}

View file

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

View file

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

View file

@ -42,8 +42,10 @@ class ErrorDialog : BaseDialogFragment<DialogErrorBinding>() {
companion object {
private const val ARGUMENT_KEY = "Data"
fun newInstance(error: Throwable) = ErrorDialog().apply {
arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, error) }
fun newInstance(error: Throwable): ErrorDialog {
return ErrorDialog().apply {
arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, error) }
}
}
}
@ -55,14 +57,12 @@ class ErrorDialog : BaseDialogFragment<DialogErrorBinding>() {
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
) = DialogErrorBinding.inflate(inflater).apply { binding = this }.root
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return DialogErrorBinding.inflate(inflater).apply { binding = this }.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
val stringWriter = StringWriter().apply {
error.printStackTrace(PrintWriter(this))
@ -114,17 +114,11 @@ class ErrorDialog : BaseDialogFragment<DialogErrorBinding>() {
chooserTitle = getString(R.string.about_feedback),
email = "wulkanowyinc@gmail.com",
subject = "Zgłoszenie błędu",
body = requireContext().getString(
R.string.about_feedback_template,
"${appInfo.systemManufacturer} ${appInfo.systemModel}",
appInfo.systemVersion.toString(),
"${appInfo.versionName}-${appInfo.buildFlavor}"
body = requireContext().getString(R.string.about_feedback_template,
"${appInfo.systemManufacturer} ${appInfo.systemModel}", appInfo.systemVersion.toString(), appInfo.versionName
) + "\n" + content,
onActivityNotFound = {
requireContext().openInternetBrowser(
"https://github.com/wulkanowy/wulkanowy/issues",
::showMessage
)
requireContext().openInternetBrowser("https://github.com/wulkanowy/wulkanowy/issues", ::showMessage)
}
)
}

View file

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

View file

@ -9,7 +9,6 @@ import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.databinding.ItemAccountBinding
import io.github.wulkanowy.utils.getThemeAttrColor
import io.github.wulkanowy.utils.nickOrName
import javax.inject.Inject
class WidgetConfigureAdapter @Inject constructor() : RecyclerView.Adapter<WidgetConfigureAdapter.ItemViewHolder>() {
@ -29,7 +28,7 @@ class WidgetConfigureAdapter @Inject constructor() : RecyclerView.Adapter<Widget
val (student, isCurrent) = items[position]
with(holder.binding) {
accountItemName.text = "${student.nickOrName} ${student.className}"
accountItemName.text = "${student.studentName} ${student.className}"
accountItemSchool.text = student.schoolName
with(accountItemImage) {

View file

@ -18,8 +18,6 @@ import io.github.wulkanowy.utils.getCompatDrawable
import io.github.wulkanowy.utils.openAppInMarket
import io.github.wulkanowy.utils.openEmailClient
import io.github.wulkanowy.utils.openInternetBrowser
import io.github.wulkanowy.utils.toFormattedString
import io.github.wulkanowy.utils.toLocalDateTime
import javax.inject.Inject
@AndroidEntryPoint
@ -37,9 +35,7 @@ class AboutFragment : BaseFragment<FragmentAboutBinding>(R.layout.fragment_about
override val versionRes: Triple<String, String, Drawable?>?
get() = context?.run {
val buildTimestamp = appInfo.buildTimestamp.toLocalDateTime().toFormattedString("yyyy-MM-dd")
val versionSignature = "${appInfo.versionName}-${appInfo.buildFlavor} (${appInfo.versionCode}), $buildTimestamp"
Triple(getString(R.string.about_version), versionSignature, getCompatDrawable(R.drawable.ic_all_about))
Triple(getString(R.string.about_version), "${appInfo.versionName} (${appInfo.versionCode})", getCompatDrawable(R.drawable.ic_all_about))
}
override val creatorsRes: Triple<String, String, Drawable?>?
@ -69,11 +65,7 @@ class AboutFragment : BaseFragment<FragmentAboutBinding>(R.layout.fragment_about
override val homepageRes: Triple<String, String, Drawable?>?
get() = context?.run {
Triple(
getString(R.string.about_homepage),
getString(R.string.about_homepage_summary),
getCompatDrawable(R.drawable.ic_all_home)
)
Triple(getString(R.string.about_homepage), getString(R.string.about_homepage_summary), getCompatDrawable(R.drawable.ic_about_homepage))
}
override val licensesRes: Triple<String, String, Drawable?>?
@ -139,17 +131,11 @@ class AboutFragment : BaseFragment<FragmentAboutBinding>(R.layout.fragment_about
chooserTitle = getString(R.string.about_feedback),
email = "wulkanowyinc@gmail.com",
subject = "Zgłoszenie błędu",
body = getString(
R.string.about_feedback_template,
"${appInfo.systemManufacturer} ${appInfo.systemModel}",
appInfo.systemVersion.toString(),
"${appInfo.versionName}-${appInfo.buildFlavor}"
body = getString(R.string.about_feedback_template,
"${appInfo.systemManufacturer} ${appInfo.systemModel}", appInfo.systemVersion.toString(), appInfo.versionName
),
onActivityNotFound = {
requireContext().openInternetBrowser(
"https://github.com/wulkanowy/wulkanowy/issues",
::showMessage
)
requireContext().openInternetBrowser("https://github.com/wulkanowy/wulkanowy/issues", ::showMessage)
}
)
}

View file

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

View file

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

View file

@ -1,25 +1,23 @@
package io.github.wulkanowy.ui.modules.account
import android.annotation.SuppressLint
import android.graphics.PorterDuff
import android.view.LayoutInflater
import android.view.View.GONE
import android.view.View.VISIBLE
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
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.HeaderAccountBinding
import io.github.wulkanowy.databinding.ItemAccountBinding
import io.github.wulkanowy.utils.createNameInitialsDrawable
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.getThemeAttrColor
import io.github.wulkanowy.utils.nickOrName
import javax.inject.Inject
class AccountAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
var isAccountQuickDialogMode = false
var items = emptyList<AccountItem<*>>()
var onClickListener: (StudentWithSemesters) -> Unit = {}
@ -32,77 +30,56 @@ class AccountAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView.V
val inflater = LayoutInflater.from(parent.context)
return when (viewType) {
AccountItem.ViewType.HEADER.id -> HeaderViewHolder(
HeaderAccountBinding.inflate(inflater, parent, false)
)
AccountItem.ViewType.ITEM.id -> ItemViewHolder(
ItemAccountBinding.inflate(inflater, parent, false)
)
AccountItem.ViewType.HEADER.id -> HeaderViewHolder(HeaderAccountBinding.inflate(inflater, parent, false))
AccountItem.ViewType.ITEM.id -> ItemViewHolder(ItemAccountBinding.inflate(inflater, parent, false))
else -> throw IllegalStateException()
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is HeaderViewHolder -> bindHeaderViewHolder(
holder.binding,
items[position].value as Account,
position
)
is ItemViewHolder -> bindItemViewHolder(
holder.binding,
items[position].value as StudentWithSemesters
)
is HeaderViewHolder -> bindHeaderViewHolder(holder.binding, items[position].value as Account)
is ItemViewHolder -> bindItemViewHolder(holder.binding, items[position].value as StudentWithSemesters)
}
}
private fun bindHeaderViewHolder(
binding: HeaderAccountBinding,
account: Account,
position: Int
) {
private fun bindHeaderViewHolder(binding: HeaderAccountBinding, account: Account) {
with(binding) {
accountHeaderDivider.visibility = if (position == 0) GONE else VISIBLE
accountHeaderEmail.text = account.email
accountHeaderType.setText(if (account.isParent) R.string.account_type_parent else R.string.account_type_student)
}
}
@SuppressLint("SetTextI18n")
private fun bindItemViewHolder(
binding: ItemAccountBinding,
studentWithSemesters: StudentWithSemesters
) {
val context = binding.root.context
private fun bindItemViewHolder(binding: ItemAccountBinding, studentWithSemesters: StudentWithSemesters) {
val student = studentWithSemesters.student
val semesters = studentWithSemesters.semesters
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 {
if (it.value !is StudentWithSemesters) return@filter false
val studentToCompare = it.value.student
studentToCompare.studentId == student.studentId
&& studentToCompare.schoolSymbol == student.schoolSymbol
&& studentToCompare.symbol == student.symbol
}.size > 1 && isAccountQuickDialogMode
with(binding) {
accountItemName.text = "${student.nickOrName} ${diary?.diaryName.orEmpty()}"
accountItemName.text = "${student.studentName} ${diary?.diaryName.orEmpty()}"
accountItemSchool.text = studentWithSemesters.student.schoolName
accountItemImage.setImageDrawable(avatar)
with(accountItemAccountType) {
setText(if (student.isParent) R.string.account_type_parent else R.string.account_type_student)
isVisible = isDuplicatedStudent
with(accountItemLoginMode) {
visibility = when (Sdk.Mode.valueOf(student.loginMode)) {
Sdk.Mode.API -> {
setText(R.string.account_login_mobile_api)
VISIBLE
}
Sdk.Mode.HYBRID -> {
setText(R.string.account_login_hybrid)
VISIBLE
}
Sdk.Mode.SCRAPPER -> {
GONE
}
}
}
with(accountItemCheck) {
isVisible = student.isCurrent
borderColor = checkBackgroundColor
circleColor = checkBackgroundColor
with(accountItemImage) {
val colorImage = if (student.isCurrent) context.getThemeAttrColor(R.attr.colorPrimary)
else context.getThemeAttrColor(R.attr.colorOnSurface, 153)
setColorFilter(colorImage, PorterDuff.Mode.SRC_IN)
}
root.setOnClickListener { onClickListener(studentWithSemesters) }

View file

@ -0,0 +1,102 @@
package io.github.wulkanowy.ui.modules.account
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import android.widget.Toast.LENGTH_LONG
import androidx.appcompat.app.AlertDialog
import androidx.recyclerview.widget.LinearLayoutManager
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.databinding.DialogAccountBinding
import io.github.wulkanowy.ui.base.BaseDialogFragment
import io.github.wulkanowy.ui.modules.login.LoginActivity
import javax.inject.Inject
@AndroidEntryPoint
class AccountDialog : BaseDialogFragment<DialogAccountBinding>(), AccountView {
@Inject
lateinit var presenter: AccountPresenter
@Inject
lateinit var accountAdapter: AccountAdapter
companion object {
fun newInstance() = AccountDialog()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NO_TITLE, 0)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return DialogAccountBinding.inflate(inflater).apply { binding = this }.root
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
presenter.onAttachView(this)
}
override fun initView() {
accountAdapter.onClickListener = presenter::onItemSelected
with(binding) {
accountDialogAdd.setOnClickListener { presenter.onAddSelected() }
accountDialogRemove.setOnClickListener { presenter.onRemoveSelected() }
accountDialogRecycler.apply {
layoutManager = LinearLayoutManager(context)
adapter = accountAdapter
}
}
}
override fun updateData(data: List<AccountItem<*>>) {
with(accountAdapter) {
items = data
notifyDataSetChanged()
}
}
override fun showError(text: String, error: Throwable) {
showMessage(text)
}
override fun showMessage(text: String) {
Toast.makeText(context, text, LENGTH_LONG).show()
}
override fun dismissView() {
dismiss()
}
override fun openLoginView() {
activity?.let {
startActivity(LoginActivity.getStartIntent(it))
}
}
override fun showConfirmDialog() {
context?.let {
AlertDialog.Builder(it)
.setTitle(R.string.account_logout_student)
.setMessage(R.string.account_confirm)
.setPositiveButton(R.string.account_logout) { _, _ -> presenter.onLogoutConfirm() }
.setNegativeButton(android.R.string.cancel) { _, _ -> }
.show()
}
}
override fun recreateMainView() {
activity?.recreate()
}
override fun onDestroy() {
presenter.onDetachView()
super.onDestroy()
}
}

View file

@ -1,83 +0,0 @@
package io.github.wulkanowy.ui.modules.account
import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.View
import androidx.core.view.get
import androidx.recyclerview.widget.LinearLayoutManager
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.databinding.FragmentAccountBinding
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.account.accountdetails.AccountDetailsFragment
import io.github.wulkanowy.ui.modules.login.LoginActivity
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView
import javax.inject.Inject
@AndroidEntryPoint
class AccountFragment : BaseFragment<FragmentAccountBinding>(R.layout.fragment_account),
AccountView, MainView.TitledView {
@Inject
lateinit var presenter: AccountPresenter
@Inject
lateinit var accountAdapter: AccountAdapter
companion object {
fun newInstance() = AccountFragment()
}
override val titleStringId = R.string.account_title
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
@Suppress("UNCHECKED_CAST")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentAccountBinding.bind(view)
presenter.onAttachView(this)
}
override fun initView() {
binding.accountRecycler.apply {
layoutManager = LinearLayoutManager(context)
adapter = accountAdapter
}
accountAdapter.onClickListener = presenter::onItemSelected
binding.accountAdd.setOnClickListener { presenter.onAddSelected() }
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
menu[0].isVisible = false
}
override fun updateData(data: List<AccountItem<*>>) {
with(accountAdapter) {
items = data
notifyDataSetChanged()
}
}
override fun openLoginView() {
activity?.let {
startActivity(LoginActivity.getStartIntent(it))
}
}
override fun openAccountDetailsView(studentWithSemesters: StudentWithSemesters) {
(activity as? MainActivity)?.pushView(
AccountDetailsFragment.newInstance(studentWithSemesters)
)
}
}

View file

@ -3,8 +3,10 @@ package io.github.wulkanowy.ui.modules.account
import io.github.wulkanowy.data.Status
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.services.sync.SyncManager
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.utils.afterLoading
import io.github.wulkanowy.utils.flowWithResource
import kotlinx.coroutines.flow.onEach
import timber.log.Timber
@ -13,12 +15,13 @@ import javax.inject.Inject
class AccountPresenter @Inject constructor(
errorHandler: ErrorHandler,
studentRepository: StudentRepository,
private val syncManager: SyncManager
) : BasePresenter<AccountView>(errorHandler, studentRepository) {
override fun onAttachView(view: AccountView) {
super.onAttachView(view)
view.initView()
Timber.i("Account view was initialized")
Timber.i("Account dialog view was initialized")
loadData()
}
@ -27,39 +30,86 @@ class AccountPresenter @Inject constructor(
view?.openLoginView()
}
fun onItemSelected(studentWithSemesters: StudentWithSemesters) {
view?.openAccountDetailsView(studentWithSemesters)
fun onRemoveSelected() {
Timber.i("Select remove account")
view?.showConfirmDialog()
}
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!!)
fun onLogoutConfirm() {
flowWithResource {
val student = studentRepository.getCurrentStudent(false)
studentRepository.logoutStudent(student)
val students = studentRepository.getSavedStudents(false)
if (students.isNotEmpty()) {
studentRepository.switchStudent(students[0])
}
students
}.onEach {
when (it.status) {
Status.LOADING -> Timber.i("Attempt to logout current user ")
Status.SUCCESS -> view?.run {
if (it.data!!.isEmpty()) {
Timber.i("Logout result: Open login view")
syncManager.stopSyncWorker()
openClearLoginView()
} else {
Timber.i("Logout result: Switch to another student")
recreateMainView()
}
}
Status.ERROR -> {
Timber.i("Logout result: An exception occurred")
errorHandler.dispatch(it.error!!)
}
}
.launch("load")
}.afterLoading {
view?.dismissView()
}.launch("logout")
}
fun onItemSelected(studentWithSemesters: StudentWithSemesters) {
Timber.i("Select student item ${studentWithSemesters.student.id}")
if (studentWithSemesters.student.isCurrent) {
view?.dismissView()
} else flowWithResource { studentRepository.switchStudent(studentWithSemesters) }.onEach {
when (it.status) {
Status.LOADING -> Timber.i("Attempt to change a student")
Status.SUCCESS -> {
Timber.i("Change a student result: Success")
view?.recreateMainView()
}
Status.ERROR -> {
Timber.i("Change a student result: An exception occurred")
errorHandler.dispatch(it.error!!)
}
}
}.afterLoading {
view?.dismissView()
}.launch("switch")
}
private fun createAccountItems(items: List<StudentWithSemesters>): List<AccountItem<*>> {
return items.groupBy {
Account("${it.student.userName} (${it.student.email})", it.student.isParent)
}
.map { (account, students) ->
listOf(
AccountItem(account, AccountItem.ViewType.HEADER)
) + students.map { student ->
AccountItem(student, AccountItem.ViewType.ITEM)
return items.groupBy { Account(it.student.email, it.student.isParent) }.map { (account, students) ->
listOf(AccountItem(account, AccountItem.ViewType.HEADER)) + students.map { student ->
AccountItem(student, AccountItem.ViewType.ITEM)
}
}.flatten()
}
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!!)
}
}
.flatten()
}.launch()
}
}

View file

@ -1,6 +1,5 @@
package io.github.wulkanowy.ui.modules.account
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.ui.base.BaseView
interface AccountView : BaseView {
@ -9,7 +8,12 @@ interface AccountView : BaseView {
fun updateData(data: List<AccountItem<*>>)
fun dismissView()
fun showConfirmDialog()
fun openLoginView()
fun openAccountDetailsView(studentWithSemesters: StudentWithSemesters)
fun recreateMainView()
}

View file

@ -1,164 +0,0 @@
package io.github.wulkanowy.ui.modules.account.accountdetails
import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import androidx.appcompat.app.AlertDialog
import androidx.core.view.get
import androidx.core.view.isVisible
import dagger.hilt.android.AndroidEntryPoint
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.FragmentAccountDetailsBinding
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.account.accountedit.AccountEditDialog
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.ui.modules.studentinfo.StudentInfoFragment
import io.github.wulkanowy.ui.modules.studentinfo.StudentInfoView
import io.github.wulkanowy.utils.createNameInitialsDrawable
import io.github.wulkanowy.utils.nickOrName
import javax.inject.Inject
@AndroidEntryPoint
class AccountDetailsFragment :
BaseFragment<FragmentAccountDetailsBinding>(R.layout.fragment_account_details),
AccountDetailsView, MainView.TitledView {
@Inject
lateinit var presenter: AccountDetailsPresenter
override val titleStringId = R.string.account_details_title
companion object {
private const val ARGUMENT_KEY = "Data"
fun newInstance(studentWithSemesters: StudentWithSemesters) =
AccountDetailsFragment().apply {
arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, studentWithSemesters) }
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentAccountDetailsBinding.bind(view)
presenter.onAttachView(this, requireArguments()[ARGUMENT_KEY] as StudentWithSemesters)
}
override fun initView() {
binding.accountDetailsErrorRetry.setOnClickListener { presenter.onRetry() }
binding.accountDetailsErrorDetails.setOnClickListener { presenter.onDetailsClick() }
binding.accountDetailsLogout.setOnClickListener { presenter.onRemoveSelected() }
binding.accountDetailsSelect.setOnClickListener { presenter.onStudentSelect() }
binding.accountDetailsPersonalData.setOnClickListener {
presenter.onStudentInfoSelected(StudentInfoView.Type.PERSONAL)
}
binding.accountDetailsAddressData.setOnClickListener {
presenter.onStudentInfoSelected(StudentInfoView.Type.ADDRESS)
}
binding.accountDetailsContactData.setOnClickListener {
presenter.onStudentInfoSelected(StudentInfoView.Type.CONTACT)
}
binding.accountDetailsFamilyData.setOnClickListener {
presenter.onStudentInfoSelected(StudentInfoView.Type.FAMILY)
}
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
menu[0].isVisible = false
inflater.inflate(R.menu.action_menu_account_details, menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return if (item.itemId == R.id.accountDetailsMenuEdit) {
presenter.onAccountEditSelected()
true
} else false
}
override fun showAccountData(student: Student) {
with(binding) {
accountDetailsCheck.isVisible = student.isCurrent
accountDetailsName.text = student.nickOrName
accountDetailsSchool.text = student.schoolName
accountDetailsAvatar.setImageDrawable(
requireContext().createNameInitialsDrawable(
student.nickOrName,
student.avatarColor
)
)
}
}
override fun enableSelectStudentButton(enable: Boolean) {
binding.accountDetailsSelect.isEnabled = enable
}
override fun showAccountEditDetailsDialog(student: Student) {
(requireActivity() as MainActivity).showDialogFragment(
AccountEditDialog.newInstance(student)
)
}
override fun showLogoutConfirmDialog() {
context?.let {
AlertDialog.Builder(it)
.setTitle(R.string.account_logout_student)
.setMessage(R.string.account_confirm)
.setPositiveButton(R.string.account_logout) { _, _ -> presenter.onLogoutConfirm() }
.setNegativeButton(android.R.string.cancel) { _, _ -> }
.show()
}
}
override fun popView() {
(requireActivity() as MainActivity).popView(2)
}
override fun recreateMainView() {
requireActivity().recreate()
}
override fun openStudentInfoView(
infoType: StudentInfoView.Type,
studentWithSemesters: StudentWithSemesters
) {
(requireActivity() as MainActivity).pushView(
StudentInfoFragment.newInstance(
infoType,
studentWithSemesters
)
)
}
override fun showErrorView(show: Boolean) {
binding.accountDetailsError.visibility = if (show) View.VISIBLE else View.GONE
}
override fun setErrorDetails(message: String) {
binding.accountDetailsErrorMessage.text = message
}
override fun showProgress(show: Boolean) {
binding.accountDetailsProgress.visibility = if (show) View.VISIBLE else View.GONE
}
override fun showContent(show: Boolean) {
binding.accountDetailsContent.visibility = if (show) View.VISIBLE else View.GONE
}
override fun onDestroyView() {
presenter.onDetachView()
super.onDestroyView()
}
}

View file

@ -1,180 +0,0 @@
package io.github.wulkanowy.ui.modules.account.accountdetails
import io.github.wulkanowy.data.Resource
import io.github.wulkanowy.data.Status
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.services.sync.SyncManager
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.ui.modules.studentinfo.StudentInfoView
import io.github.wulkanowy.utils.afterLoading
import io.github.wulkanowy.utils.flowWithResource
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import timber.log.Timber
import javax.inject.Inject
class AccountDetailsPresenter @Inject constructor(
errorHandler: ErrorHandler,
studentRepository: StudentRepository,
private val syncManager: SyncManager
) : BasePresenter<AccountDetailsView>(errorHandler, studentRepository) {
private var studentWithSemesters: StudentWithSemesters? = null
private lateinit var lastError: Throwable
private var studentId: Long? = null
fun onAttachView(view: AccountDetailsView, studentWithSemesters: StudentWithSemesters) {
super.onAttachView(view)
studentId = studentWithSemesters.student.id
view.initView()
errorHandler.showErrorMessage = ::showErrorViewOnError
Timber.i("Account details view was initialized")
loadData()
}
fun onRetry() {
view?.run {
showErrorView(false)
showProgress(true)
}
loadData()
}
fun onDetailsClick() {
view?.showErrorDetailsDialog(lastError)
}
private fun loadData() {
flowWithResource { studentRepository.getSavedStudents() }
.map { studentWithSemesters ->
Resource(
data = studentWithSemesters.data?.single { it.student.id == studentId },
status = studentWithSemesters.status,
error = studentWithSemesters.error
)
}
.onEach {
when (it.status) {
Status.LOADING -> {
view?.run {
showProgress(true)
showContent(false)
}
Timber.i("Loading account details view started")
}
Status.SUCCESS -> {
Timber.i("Loading account details view result: Success")
studentWithSemesters = it.data
view?.run {
showAccountData(studentWithSemesters!!.student)
enableSelectStudentButton(!studentWithSemesters!!.student.isCurrent)
showContent(true)
showErrorView(false)
}
}
Status.ERROR -> {
Timber.i("Loading account details view result: An exception occurred")
errorHandler.dispatch(it.error!!)
}
}
}
.afterLoading { view?.showProgress(false) }
.launch()
}
fun onAccountEditSelected() {
studentWithSemesters?.let {
view?.showAccountEditDetailsDialog(it.student)
}
}
fun onStudentInfoSelected(infoType: StudentInfoView.Type) {
studentWithSemesters?.let {
view?.openStudentInfoView(infoType, it)
}
}
fun onStudentSelect() {
if (studentWithSemesters == null) return
Timber.i("Select student ${studentWithSemesters!!.student.id}")
flowWithResource { studentRepository.switchStudent(studentWithSemesters!!) }
.onEach {
when (it.status) {
Status.LOADING -> Timber.i("Attempt to change a student")
Status.SUCCESS -> {
Timber.i("Change a student result: Success")
view?.recreateMainView()
}
Status.ERROR -> {
Timber.i("Change a student result: An exception occurred")
errorHandler.dispatch(it.error!!)
}
}
}.afterLoading {
view?.popView()
}.launch("switch")
}
fun onRemoveSelected() {
Timber.i("Select remove account")
view?.showLogoutConfirmDialog()
}
fun onLogoutConfirm() {
if (studentWithSemesters == null) return
flowWithResource {
val studentToLogout = studentWithSemesters!!.student
studentRepository.logoutStudent(studentToLogout)
val students = studentRepository.getSavedStudents(false)
if (studentToLogout.isCurrent && students.isNotEmpty()) {
studentRepository.switchStudent(students[0])
}
return@flowWithResource students
}.onEach {
when (it.status) {
Status.LOADING -> Timber.i("Attempt to logout user")
Status.SUCCESS -> view?.run {
when {
it.data!!.isEmpty() -> {
Timber.i("Logout result: Open login view")
syncManager.stopSyncWorker()
openClearLoginView()
}
studentWithSemesters!!.student.isCurrent -> {
Timber.i("Logout result: Logout student and switch to another")
recreateMainView()
}
else -> Timber.i("Logout result: Logout student")
}
}
Status.ERROR -> {
Timber.i("Logout result: An exception occurred")
errorHandler.dispatch(it.error!!)
}
}
}.afterLoading {
view?.popView()
}.launch("logout")
}
private fun showErrorViewOnError(message: String, error: Throwable) {
view?.run {
lastError = error
setErrorDetails(message)
showErrorView(true)
showContent(false)
showProgress(false)
}
}
}

View file

@ -1,36 +0,0 @@
package io.github.wulkanowy.ui.modules.account.accountdetails
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.modules.studentinfo.StudentInfoView
interface AccountDetailsView : BaseView {
fun initView()
fun showAccountData(student: Student)
fun showAccountEditDetailsDialog(student: Student)
fun showLogoutConfirmDialog()
fun popView()
fun recreateMainView()
fun enableSelectStudentButton(enable: Boolean)
fun openStudentInfoView(
infoType: StudentInfoView.Type,
studentWithSemesters: StudentWithSemesters
)
fun showErrorView(show: Boolean)
fun setErrorDetails(message: String)
fun showProgress(show: Boolean)
fun showContent(show: Boolean)
}

View file

@ -1,90 +0,0 @@
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

@ -1,98 +0,0 @@
package io.github.wulkanowy.ui.modules.account.accountedit
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.GridLayoutManager
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.databinding.DialogAccountEditBinding
import io.github.wulkanowy.ui.base.BaseDialogFragment
import javax.inject.Inject
@AndroidEntryPoint
class AccountEditDialog : BaseDialogFragment<DialogAccountEditBinding>(), AccountEditView {
@Inject
lateinit var presenter: AccountEditPresenter
@Inject
lateinit var accountEditColorAdapter: AccountEditColorAdapter
companion object {
private const val ARGUMENT_KEY = "student_with_semesters"
fun newInstance(student: Student) =
AccountEditDialog().apply {
arguments = Bundle().apply {
putSerializable(ARGUMENT_KEY, student)
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NO_TITLE, 0)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View = DialogAccountEditBinding.inflate(inflater).apply { binding = this }.root
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
presenter.onAttachView(this, requireArguments()[ARGUMENT_KEY] as Student)
}
override fun initView() {
with(binding) {
accountEditDetailsCancel.setOnClickListener { dismiss() }
accountEditDetailsSave.setOnClickListener {
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()
}
}
override fun showCurrentNick(nick: String) {
binding.accountEditDetailsNickText.setText(nick)
}
override fun popView() {
dismiss()
}
override fun recreateMainView() {
activity?.recreate()
}
override fun onDestroyView() {
super.onDestroyView()
presenter.onDetachView()
}
}

View file

@ -1,80 +0,0 @@
package io.github.wulkanowy.ui.modules.account.accountedit
import io.github.wulkanowy.data.Status
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentNickAndAvatar
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.AppInfo
import io.github.wulkanowy.utils.afterLoading
import io.github.wulkanowy.utils.flowWithResource
import kotlinx.coroutines.flow.onEach
import timber.log.Timber
import javax.inject.Inject
class AccountEditPresenter @Inject constructor(
private val appInfo: AppInfo,
errorHandler: ErrorHandler,
studentRepository: StudentRepository
) : BasePresenter<AccountEditView>(errorHandler, studentRepository) {
lateinit var student: Student
private val colors = appInfo.defaultColorsForAvatar.map { it.toInt() }
fun onAttachView(view: AccountEditView, student: Student) {
super.onAttachView(view)
this.student = student
with(view) {
initView()
showCurrentNick(student.nick.trim())
}
Timber.i("Account edit dialog view was initialized")
loadData()
view.updateColorsData(colors)
}
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 {
val studentNick =
StudentNickAndAvatar(nick = nick.trim(), avatarColor = avatarColor.toLong())
.apply { id = student.id }
studentRepository.updateStudentNickAndAvatar(studentNick)
}.onEach {
when (it.status) {
Status.LOADING -> Timber.i("Attempt to change a student nick and avatar")
Status.SUCCESS -> {
Timber.i("Change a student nick and avatar result: Success")
view?.recreateMainView()
}
Status.ERROR -> {
Timber.i("Change a student nick and avatar result: An exception occurred")
errorHandler.dispatch(it.error!!)
}
}
}
.afterLoading { view?.popView() }
.launch("update_student")
}
}

View file

@ -1,18 +0,0 @@
package io.github.wulkanowy.ui.modules.account.accountedit
import io.github.wulkanowy.ui.base.BaseView
interface AccountEditView : BaseView {
fun initView()
fun popView()
fun recreateMainView()
fun showCurrentNick(nick: String)
fun updateSelectedColorData(color: Int)
fun updateColorsData(colors: List<Int>)
}

View file

@ -1,95 +0,0 @@
package io.github.wulkanowy.ui.modules.account.accountquick
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.databinding.DialogAccountQuickBinding
import io.github.wulkanowy.ui.base.BaseDialogFragment
import io.github.wulkanowy.ui.modules.account.AccountAdapter
import io.github.wulkanowy.ui.modules.account.AccountFragment
import io.github.wulkanowy.ui.modules.account.AccountItem
import io.github.wulkanowy.ui.modules.main.MainActivity
import javax.inject.Inject
@AndroidEntryPoint
class AccountQuickDialog : BaseDialogFragment<DialogAccountQuickBinding>(), AccountQuickView {
@Inject
lateinit var accountAdapter: AccountAdapter
@Inject
lateinit var presenter: AccountQuickPresenter
companion object {
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?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NO_TITLE, 0)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
) = DialogAccountQuickBinding.inflate(inflater).apply { binding = this }.root
@Suppress("UNCHECKED_CAST")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val studentsWithSemesters =
(requireArguments()[STUDENTS_ARGUMENT_KEY] as Array<StudentWithSemesters>).toList()
presenter.onAttachView(this, studentsWithSemesters)
}
override fun initView() {
binding.accountQuickDialogManger.setOnClickListener { presenter.onManagerSelected() }
with(accountAdapter) {
isAccountQuickDialogMode = true
onClickListener = presenter::onStudentSelect
}
with(binding.accountQuickDialogRecycler) {
layoutManager = LinearLayoutManager(context)
adapter = accountAdapter
}
}
override fun updateData(data: List<AccountItem<*>>) {
with(accountAdapter) {
items = data
notifyDataSetChanged()
}
}
override fun popView() {
dismiss()
}
override fun recreateMainView() {
activity?.recreate()
}
override fun openAccountView() {
(requireActivity() as MainActivity).pushView(AccountFragment.newInstance())
}
override fun onDestroyView() {
presenter.onDetachView()
super.onDestroyView()
}
}

View file

@ -1,67 +0,0 @@
package io.github.wulkanowy.ui.modules.account.accountquick
import io.github.wulkanowy.data.Status
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
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.ui.modules.account.AccountItem
import io.github.wulkanowy.utils.afterLoading
import io.github.wulkanowy.utils.flowWithResource
import kotlinx.coroutines.flow.onEach
import timber.log.Timber
import javax.inject.Inject
class AccountQuickPresenter @Inject constructor(
errorHandler: ErrorHandler,
studentRepository: StudentRepository
) : BasePresenter<AccountQuickView>(errorHandler, studentRepository) {
private lateinit var studentsWithSemesters: List<StudentWithSemesters>
fun onAttachView(view: AccountQuickView, studentsWithSemesters: List<StudentWithSemesters>) {
super.onAttachView(view)
this.studentsWithSemesters = studentsWithSemesters
view.initView()
Timber.i("Account quick dialog view was initialized")
view.updateData(createAccountItems(studentsWithSemesters))
}
fun onManagerSelected() {
view?.run {
openAccountView()
popView()
}
}
fun onStudentSelect(studentWithSemesters: StudentWithSemesters) {
Timber.i("Select student ${studentWithSemesters.student.id}")
if (studentWithSemesters.student.isCurrent) {
view?.popView()
return
}
flowWithResource { studentRepository.switchStudent(studentWithSemesters) }
.onEach {
when (it.status) {
Status.LOADING -> Timber.i("Attempt to change a student")
Status.SUCCESS -> {
Timber.i("Change a student result: Success")
view?.recreateMainView()
}
Status.ERROR -> {
Timber.i("Change a student result: An exception occurred")
errorHandler.dispatch(it.error!!)
}
}
}
.afterLoading { view?.popView() }
.launch("switch")
}
private fun createAccountItems(items: List<StudentWithSemesters>) = items.map {
AccountItem(it, AccountItem.ViewType.ITEM)
}
}

View file

@ -1,17 +0,0 @@
package io.github.wulkanowy.ui.modules.account.accountquick
import io.github.wulkanowy.ui.base.BaseView
import io.github.wulkanowy.ui.modules.account.AccountItem
interface AccountQuickView : BaseView {
fun initView()
fun updateData(data: List<AccountItem<*>>)
fun recreateMainView()
fun popView()
fun openAccountView()
}

View file

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

View file

@ -26,7 +26,6 @@ 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
@ -61,7 +60,6 @@ class AttendanceFragment : BaseFragment<FragmentAttendanceBinding>(R.layout.frag
override val excuseActionMode: Boolean get() = attendanceAdapter.excuseActionMode
private var actionMode: ActionMode? = null
private val actionModeCallback = object : ActionMode.Callback {
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
val inflater = mode.menuInflater
@ -113,8 +111,6 @@ class AttendanceFragment : BaseFragment<FragmentAttendanceBinding>(R.layout.frag
with(binding) {
attendanceSwipe.setOnRefreshListener(presenter::onSwipeRefresh)
attendanceSwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary))
attendanceSwipe.setProgressBackgroundColorSchemeColor(requireContext().getThemeAttrColor(R.attr.colorSwipeRefresh))
attendanceErrorRetry.setOnClickListener { presenter.onRetry() }
attendanceErrorDetails.setOnClickListener { presenter.onDetailsClick() }
@ -192,7 +188,7 @@ class AttendanceFragment : BaseFragment<FragmentAttendanceBinding>(R.layout.frag
}
override fun showContent(show: Boolean) {
binding.attendanceRecycler.visibility = if (show) VISIBLE else GONE
binding. attendanceRecycler.visibility = if (show) VISIBLE else GONE
}
override fun showRefresh(show: Boolean) {
@ -226,7 +222,6 @@ class AttendanceFragment : BaseFragment<FragmentAttendanceBinding>(R.layout.frag
setDateRangeLimiter(SchooldaysRangeLimiter())
version = DatePickerDialog.Version.VERSION_2
scrollOrientation = DatePickerDialog.ScrollOrientation.VERTICAL
vibrate(false)
show(this@AttendanceFragment.parentFragmentManager, null)
}
}

View file

@ -190,48 +190,35 @@ class AttendancePresenter @Inject constructor(
flowWithResourceIn {
val student = studentRepository.getCurrentStudent()
val semester = semesterRepository.getCurrentSemester(student)
attendanceRepository.getAttendance(
student,
semester,
currentDate,
currentDate,
forceRefresh
)
attendanceRepository.getAttendance(student, semester, currentDate, currentDate, forceRefresh)
}.onEach {
when (it.status) {
Status.LOADING -> {
view?.showExcuseButton(false)
if (!it.data.isNullOrEmpty()) {
val filteredAttendance = if (prefRepository.isShowPresent) {
it.data
} else {
it.data.filter { item -> !item.presence }
}
view?.run {
enableSwipe(true)
showRefresh(true)
showProgress(false)
showEmpty(filteredAttendance.isEmpty())
showContent(filteredAttendance.isNotEmpty())
updateData(filteredAttendance.sortedBy { item -> item.number })
showContent(true)
updateData(it.data.let { items ->
if (prefRepository.isShowPresent) items
else items.filter { item -> !item.presence }
}.sortedBy { item -> item.number })
}
}
}
Status.SUCCESS -> {
Timber.i("Loading attendance result: Success")
val filteredAttendance = if (prefRepository.isShowPresent) {
it.data.orEmpty()
} else {
it.data?.filter { item -> !item.presence }.orEmpty()
}
view?.apply {
updateData(filteredAttendance.sortedBy { item -> item.number })
showEmpty(filteredAttendance.isEmpty())
updateData(it.data!!.let { items ->
if (prefRepository.isShowPresent) items
else items.filter { item -> !item.presence }
}.sortedBy { item -> item.number })
showEmpty(it.data.isEmpty())
showErrorView(false)
showContent(filteredAttendance.isNotEmpty())
showExcuseButton(filteredAttendance.any { item -> item.excusable })
showContent(it.data.isNotEmpty())
showExcuseButton(it.data.any { item -> item.excusable })
}
analytics.logEvent(
"load_data",

View file

@ -15,7 +15,6 @@ import io.github.wulkanowy.databinding.FragmentAttendanceSummaryBinding
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.dpToPx
import io.github.wulkanowy.utils.getThemeAttrColor
import io.github.wulkanowy.utils.setOnItemSelectedListener
import javax.inject.Inject
@ -57,8 +56,6 @@ class AttendanceSummaryFragment :
with(binding) {
attendanceSummarySwipe.setOnRefreshListener(presenter::onSwipeRefresh)
attendanceSummarySwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary))
attendanceSummarySwipe.setProgressBackgroundColorSchemeColor(requireContext().getThemeAttrColor(R.attr.colorSwipeRefresh))
attendanceSummaryErrorRetry.setOnClickListener { presenter.onRetry() }
attendanceSummaryErrorDetails.setOnClickListener { presenter.onDetailsClick() }
}

View file

@ -10,7 +10,6 @@ import io.github.wulkanowy.databinding.FragmentConferenceBinding
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.getThemeAttrColor
import javax.inject.Inject
@AndroidEntryPoint
@ -48,9 +47,7 @@ class ConferenceFragment : BaseFragment<FragmentConferenceBinding>(R.layout.frag
}
with(binding) {
conferenceSwipe.setOnRefreshListener(presenter::onSwipeRefresh)
conferenceSwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary))
conferenceSwipe.setProgressBackgroundColorSchemeColor(requireContext().getThemeAttrColor(R.attr.colorSwipeRefresh))
conferenceSwipe.setOnRefreshListener { presenter.onSwipeRefresh() }
conferenceErrorRetry.setOnClickListener { presenter.onRetry() }
conferenceErrorDetails.setOnClickListener { presenter.onDetailsClick() }
}

View file

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

View file

@ -15,7 +15,6 @@ import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.ui.widgets.DividerItemDecoration
import io.github.wulkanowy.utils.dpToPx
import io.github.wulkanowy.utils.getThemeAttrColor
import javax.inject.Inject
@AndroidEntryPoint
@ -56,8 +55,6 @@ class ExamFragment : BaseFragment<FragmentExamBinding>(R.layout.fragment_exam),
with(binding) {
examSwipe.setOnRefreshListener(presenter::onSwipeRefresh)
examSwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary))
examSwipe.setProgressBackgroundColorSchemeColor(requireContext().getThemeAttrColor(R.attr.colorSwipeRefresh))
examErrorRetry.setOnClickListener { presenter.onRetry() }
examErrorDetails.setOnClickListener { presenter.onDetailsClick() }

View file

@ -1,7 +1,6 @@
package io.github.wulkanowy.ui.modules.grade
import io.github.wulkanowy.data.Resource
import io.github.wulkanowy.data.Status
import io.github.wulkanowy.data.db.entities.Grade
import io.github.wulkanowy.data.db.entities.GradeSummary
import io.github.wulkanowy.data.db.entities.Semester
@ -34,169 +33,81 @@ class GradeAverageProvider @Inject constructor(
private val minusModifier get() = preferencesRepository.gradeMinusModifier
fun getGradesDetailsWithAverage(student: Student, semesterId: Int, forceRefresh: Boolean) =
flowWithResourceIn {
val semesters = semesterRepository.getSemesters(student)
fun getGradesDetailsWithAverage(student: Student, semesterId: Int, forceRefresh: Boolean) = flowWithResourceIn {
val semesters = semesterRepository.getSemesters(student)
when (preferencesRepository.gradeAverageMode) {
ONE_SEMESTER -> getGradeSubjects(
student = student,
semester = semesters.single { it.semesterId == semesterId },
forceRefresh = forceRefresh
)
BOTH_SEMESTERS -> calculateCombinedAverage(
student = student,
semesters = semesters,
semesterId = semesterId,
forceRefresh = forceRefresh,
averageMode = BOTH_SEMESTERS
)
ALL_YEAR -> calculateCombinedAverage(
student = student,
semesters = semesters,
semesterId = semesterId,
forceRefresh = forceRefresh,
averageMode = ALL_YEAR
)
}
}.distinctUntilChanged()
when (preferencesRepository.gradeAverageMode) {
ONE_SEMESTER -> getSemesterDetailsWithAverage(student, semesters.single { it.semesterId == semesterId }, forceRefresh)
BOTH_SEMESTERS -> calculateBothSemestersAverage(student, semesters, semesterId, forceRefresh)
ALL_YEAR -> calculateAllYearAverage(student, semesters, semesterId, forceRefresh)
}
}.distinctUntilChanged()
private fun calculateCombinedAverage(
student: Student,
semesters: List<Semester>,
semesterId: Int,
forceRefresh: Boolean,
averageMode: GradeAverageMode
): Flow<Resource<List<GradeSubject>>> {
val gradeAverageForceCalc = preferencesRepository.gradeAverageForceCalc
private fun calculateBothSemestersAverage(student: Student, semesters: List<Semester>, semesterId: Int, forceRefresh: Boolean): Flow<Resource<List<GradeDetailsWithAverage>>> {
val selectedSemester = semesters.single { it.semesterId == semesterId }
val firstSemester =
semesters.single { it.diaryId == selectedSemester.diaryId && it.semesterName == 1 }
val firstSemester = semesters.single { it.diaryId == selectedSemester.diaryId && it.semesterName == 1 }
val selectedSemesterGradeSubjects =
getGradeSubjects(student, selectedSemester, forceRefresh)
val selectedSemesterDetailsWithAverage = getSemesterDetailsWithAverage(student, selectedSemester, forceRefresh)
if (selectedSemester == firstSemester) return selectedSemesterGradeSubjects
val firstSemesterGradeSubjects =
getGradeSubjects(student, firstSemester, forceRefresh)
return selectedSemesterGradeSubjects.combine(firstSemesterGradeSubjects) { secondSemesterGradeSubject, firstSemesterGradeSubject ->
if (firstSemesterGradeSubject.status == Status.ERROR) {
return@combine firstSemesterGradeSubject
return if (selectedSemester == firstSemester) selectedSemesterDetailsWithAverage else {
val firstSemesterDetailsWithAverage = getSemesterDetailsWithAverage(student, firstSemester, forceRefresh)
selectedSemesterDetailsWithAverage.combine(firstSemesterDetailsWithAverage) { selectedDetails, secondDetails ->
val isAnyAverage = selectedDetails.data.orEmpty().any { it.average != .0 }
secondDetails.copy(data = selectedDetails.data?.map { selected ->
val second = secondDetails.data.orEmpty().singleOrNull { it.subject == selected.subject }
selected.copy(average = if (!isAnyAverage || preferencesRepository.gradeAverageForceCalc) {
val selectedGrades = selected.grades.updateModifiers(student).calcAverage()
(selectedGrades + (second?.grades?.updateModifiers(student)?.calcAverage() ?: selectedGrades)) / 2
} else (selected.average + (second?.average ?: selected.average)) / 2)
})
}
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 firstSemesterSubject = firstSemesterGradeSubject.data.orEmpty()
.singleOrNull { it.subject == secondSemesterSubject.subject }
val updatedAverage = if (averageMode == ALL_YEAR) {
calculateAllYearAverage(
student = student,
isAnyVulcanAverage = isAnyVulcanAverageInFirstSemester || isAnyVulcanAverageInSecondSemester,
gradeAverageForceCalc = gradeAverageForceCalc,
secondSemesterSubject = secondSemesterSubject,
firstSemesterSubject = firstSemesterSubject
)
} else {
calculateBothSemestersAverage(
student = student,
isAnyVulcanAverage = isAnyVulcanAverageInFirstSemester || isAnyVulcanAverageInSecondSemester,
gradeAverageForceCalc = gradeAverageForceCalc,
secondSemesterSubject = secondSemesterSubject,
firstSemesterSubject = firstSemesterSubject
)
}
secondSemesterSubject.copy(average = updatedAverage)
}
secondSemesterGradeSubject.copy(data = updatedData)
}
}
private fun calculateAllYearAverage(
student: Student,
isAnyVulcanAverage: Boolean,
gradeAverageForceCalc: Boolean,
secondSemesterSubject: GradeSubject,
firstSemesterSubject: GradeSubject?
) = if (!isAnyVulcanAverage || gradeAverageForceCalc) {
val updatedSecondSemesterGrades =
secondSemesterSubject.grades.updateModifiers(student)
val updatedFirstSemesterGrades =
firstSemesterSubject?.grades?.updateModifiers(student).orEmpty()
private fun calculateAllYearAverage(student: Student, semesters: List<Semester>, semesterId: Int, forceRefresh: Boolean): Flow<Resource<List<GradeDetailsWithAverage>>> {
val selectedSemester = semesters.single { it.semesterId == semesterId }
val firstSemester = semesters.single { it.diaryId == selectedSemester.diaryId && it.semesterName == 1 }
(updatedSecondSemesterGrades + updatedFirstSemesterGrades).calcAverage()
} else {
secondSemesterSubject.average
}
val selectedSemesterDetailsWithAverage = getSemesterDetailsWithAverage(student, selectedSemester, forceRefresh)
private fun calculateBothSemestersAverage(
student: Student,
isAnyVulcanAverage: Boolean,
gradeAverageForceCalc: Boolean,
secondSemesterSubject: GradeSubject,
firstSemesterSubject: GradeSubject?
): Double {
val divider = if (secondSemesterSubject.grades.any { it.weightValue > .0 }) 2 else 1
return if (!isAnyVulcanAverage || gradeAverageForceCalc) {
val secondSemesterAverage =
secondSemesterSubject.grades.updateModifiers(student).calcAverage()
val firstSemesterAverage = firstSemesterSubject?.grades?.updateModifiers(student)
?.calcAverage() ?: secondSemesterAverage
(secondSemesterAverage + firstSemesterAverage) / divider
} else {
(secondSemesterSubject.average + (firstSemesterSubject?.average ?: secondSemesterSubject.average)) / divider
return if (selectedSemester == firstSemester) selectedSemesterDetailsWithAverage else {
val firstSemesterDetailsWithAverage = getSemesterDetailsWithAverage(student, firstSemester, forceRefresh)
selectedSemesterDetailsWithAverage.combine(firstSemesterDetailsWithAverage) { selectedDetails, secondDetails ->
val isAnyAverage = selectedDetails.data.orEmpty().any { it.average != .0 }
secondDetails.copy(data = selectedDetails.data?.map { selected ->
val second = secondDetails.data.orEmpty().singleOrNull { it.subject == selected.subject }
selected.copy(average = if (!isAnyAverage || preferencesRepository.gradeAverageForceCalc) {
(selected.grades.updateModifiers(student) + second?.grades?.updateModifiers(student).orEmpty()).calcAverage()
} else selected.average)
})
}
}
}
private fun getGradeSubjects(
student: Student,
semester: Semester,
forceRefresh: Boolean
): Flow<Resource<List<GradeSubject>>> {
val gradeAverageForceCalc = preferencesRepository.gradeAverageForceCalc
private fun getSemesterDetailsWithAverage(student: Student, semester: Semester, forceRefresh: Boolean): Flow<Resource<List<GradeDetailsWithAverage>>> {
return gradeRepository.getGrades(student, semester, forceRefresh = forceRefresh).map { res ->
val (details, summaries) = res.data ?: null to null
val isAnyAverage = summaries.orEmpty().any { it.average != .0 }
val allGrades = details.orEmpty().groupBy { it.subject }
return gradeRepository.getGrades(student, semester, forceRefresh = forceRefresh)
.map { res ->
val (details, summaries) = res.data ?: null to null
val isAnyAverage = summaries.orEmpty().any { it.average != .0 }
val allGrades = details.orEmpty().groupBy { it.subject }
val items = summaries?.emulateEmptySummaries(
student,
semester,
allGrades.toList(),
isAnyAverage
)?.map { summary ->
val grades = allGrades[summary.subject].orEmpty()
GradeSubject(
subject = summary.subject,
average = if (!isAnyAverage || gradeAverageForceCalc) {
grades.updateModifiers(student).calcAverage()
} else summary.average,
points = summary.pointsSum,
summary = summary,
grades = grades
)
}
Resource(res.status, items, res.error)
val items = summaries?.emulateEmptySummaries(student, semester, allGrades.toList(), isAnyAverage)?.map { summary ->
val grades = allGrades[summary.subject].orEmpty()
GradeDetailsWithAverage(
subject = summary.subject,
average = if (!isAnyAverage || preferencesRepository.gradeAverageForceCalc) {
grades.updateModifiers(student).calcAverage()
} else summary.average,
points = summary.pointsSum,
summary = summary,
grades = grades
)
}
Resource(res.status, items, res.error)
}
}
private fun List<GradeSummary>.emulateEmptySummaries(
student: Student,
semester: Semester,
grades: List<Pair<String, List<Grade>>>,
calcAverage: Boolean
): List<GradeSummary> {
private fun List<GradeSummary>.emulateEmptySummaries(student: Student, semester: Semester, grades: List<Pair<String, List<Grade>>>, calcAverage: Boolean): List<GradeSummary> {
if (isNotEmpty() && size > grades.size) return this
return grades.mapIndexed { i, (subject, details) ->

View file

@ -3,7 +3,7 @@ package io.github.wulkanowy.ui.modules.grade
import io.github.wulkanowy.data.db.entities.Grade
import io.github.wulkanowy.data.db.entities.GradeSummary
data class GradeSubject(
data class GradeDetailsWithAverage(
val subject: String,
val average: Double,
val points: String,

View file

@ -10,7 +10,6 @@ import android.view.View.VISIBLE
import androidx.appcompat.app.AlertDialog
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.databinding.FragmentGradeBinding
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.base.BaseFragmentPagerAdapter
@ -34,6 +33,7 @@ class GradeFragment : BaseFragment<FragmentGradeBinding>(R.layout.fragment_grade
private var semesterSwitchMenu: MenuItem? = null
companion object {
private const val SAVED_SEMESTER_KEY = "CURRENT_SEMESTER"
fun newInstance() = GradeFragment()
}
@ -52,7 +52,7 @@ class GradeFragment : BaseFragment<FragmentGradeBinding>(R.layout.fragment_grade
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentGradeBinding.bind(view)
presenter.onAttachView(this)
presenter.onAttachView(this, savedInstanceState?.getInt(SAVED_SEMESTER_KEY))
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
@ -64,13 +64,11 @@ class GradeFragment : BaseFragment<FragmentGradeBinding>(R.layout.fragment_grade
override fun initView() {
with(pagerAdapter) {
containerId = binding.gradeViewPager.id
addFragmentsWithTitle(
mapOf(
GradeDetailsFragment.newInstance() to getString(R.string.all_details),
GradeSummaryFragment.newInstance() to getString(R.string.grade_menu_summary),
GradeStatisticsFragment.newInstance() to getString(R.string.grade_menu_statistics)
)
)
addFragmentsWithTitle(mapOf(
GradeDetailsFragment.newInstance() to getString(R.string.all_details),
GradeSummaryFragment.newInstance() to getString(R.string.grade_menu_summary),
GradeStatisticsFragment.newInstance() to getString(R.string.grade_menu_statistics)
))
}
with(binding.gradeViewPager) {
@ -122,9 +120,11 @@ class GradeFragment : BaseFragment<FragmentGradeBinding>(R.layout.fragment_grade
semesterSwitchMenu?.isVisible = show
}
override fun showSemesterDialog(selectedIndex: Int, semesters: List<Semester>) {
val choices = semesters.map { getString(R.string.grade_semester, it.semesterName) }
.toTypedArray()
override fun showSemesterDialog(selectedIndex: Int) {
val choices = arrayOf(
getString(R.string.grade_semester, 1),
getString(R.string.grade_semester, 2)
)
AlertDialog.Builder(requireContext())
.setSingleChoiceItems(choices, selectedIndex) { dialog, which ->
@ -138,10 +138,7 @@ class GradeFragment : BaseFragment<FragmentGradeBinding>(R.layout.fragment_grade
override fun setCurrentSemesterName(semester: Int, schoolYear: Int) {
subtitleString = getString(R.string.grade_subtitle, semester, schoolYear, schoolYear + 1)
if (isVisible) {
(activity as MainView?)?.setViewSubTitle(subtitleString)
}
(activity as MainView).setViewSubTitle(subtitleString)
}
fun onChildRefresh() {
@ -153,8 +150,7 @@ class GradeFragment : BaseFragment<FragmentGradeBinding>(R.layout.fragment_grade
}
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) {
@ -165,6 +161,11 @@ class GradeFragment : BaseFragment<FragmentGradeBinding>(R.layout.fragment_grade
(pagerAdapter.getFragmentInstance(index) as? GradeView.GradeChildView)?.onParentChangeSemester()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putInt(SAVED_SEMESTER_KEY, presenter.selectedIndex)
}
override fun onDestroyView() {
presenter.onDetachView()
super.onDestroyView()

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