1
0

Compare commits

...

80 Commits

Author SHA1 Message Date
d3a6ea5acf Merge branch 'release/0.18.3' 2020-06-10 19:18:23 +02:00
674a78b661 Version 0.18.3 2020-06-10 19:18:09 +02:00
5c84c8d5b1 Fix force average calc from two semesters (#870) 2020-06-10 17:28:49 +02:00
522a36d670 Fix message deleting (#875) 2020-06-10 16:26:45 +02:00
2d0cfc3e8e Merge tag '0.18.2' into develop
Version 0.18.2
2020-06-02 17:08:02 +02:00
4b6b722f87 Merge branch 'release/0.18.2' 2020-06-02 17:07:58 +02:00
419675066f Version 0.18.2 2020-06-02 17:07:52 +02:00
191b1ad022 Emulate summaries from grade list when summaries are empty (#855) 2020-06-02 15:51:15 +02:00
792e44a9d0 Fix login button state in student select login fragment (#863) 2020-06-02 15:50:32 +02:00
ff5a47b0df New Crowdin translations (#856) 2020-06-02 15:18:41 +02:00
7bf0acb703 Revert "Bump sonarqube-gradle-plugin from 2.8 to 3.0" (#864)
This reverts commit ab7d30c995.
2020-06-02 15:13:41 +02:00
ba5dbf90d8 Fixes in updating adapter items (#854) 2020-06-02 01:04:41 +02:00
54f41aaa63 Fix too many alarms on samsung devices (#859) 2020-06-02 01:04:02 +02:00
1db42210e8 Bump about_libraries from 8.1.4 to 8.1.6 (#861)
Bumps `about_libraries` from 8.1.4 to 8.1.6.

Updates `aboutlibraries-plugin` from 8.1.4 to 8.1.6
- [Release notes](https://github.com/mikepenz/AboutLibraries/releases)
- [Changelog](https://github.com/mikepenz/AboutLibraries/blob/develop/gradle-release.gradle)
- [Commits](https://github.com/mikepenz/AboutLibraries/compare/v8.1.4...v8.1.6)

Updates `aboutlibraries-core` from 8.1.4 to 8.1.6
- [Release notes](https://github.com/mikepenz/AboutLibraries/releases)
- [Changelog](https://github.com/mikepenz/AboutLibraries/blob/develop/gradle-release.gradle)
- [Commits](https://github.com/mikepenz/AboutLibraries/compare/v8.1.4...v8.1.6)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-06-02 01:02:16 +02:00
fb554a4a3b Fix capitalization in new message activity (#860) 2020-06-02 01:01:58 +02:00
d8d13c73fb Filter out empty items in grade summary (#857) 2020-06-02 01:01:02 +02:00
5c0160a24d Don't capture click on login student select checkbox (#862) 2020-06-02 00:57:22 +02:00
ab7d30c995 Bump sonarqube-gradle-plugin from 2.8 to 3.0 (#853) 2020-05-30 12:19:52 +00:00
1cfa1f15c0 Bump gradle from 3.6.3 to 4.0.0 (#852) 2020-05-30 11:33:43 +00:00
2149a4db9f Bump about_libraries from 8.1.3 to 8.1.4 (#851) 2020-05-30 11:17:22 +00:00
df57d16d21 Bump dagger from 2.27 to 2.28 (#850) 2020-05-30 11:16:58 +00:00
2ff031005e Fix displaying the feature disabled message in completed lessons (#849) 2020-05-30 13:15:28 +02:00
b9ab85ee55 Merge tag '0.18.1' into develop
Version 0.18.1
2020-05-24 21:20:28 +02:00
064998129e Merge branch 'release/0.18.1' 2020-05-24 21:20:23 +02:00
4044cdd9a5 Version 0.18.1 2020-05-24 21:20:17 +02:00
1ee10a5902 New Crowdin translations (#847) 2020-05-24 21:18:49 +02:00
27b1d076c7 Bump firebase-inappmessaging-ktx from 19.0.6 to 19.0.7 (#844) 2020-05-24 21:03:35 +02:00
c8b32fdb3b Bump firebase-analytics from 17.4.1 to 17.4.2 (#843) 2020-05-24 21:02:42 +02:00
0b4434fdb6 Bump firebase-crashlytics-gradle from 2.1.0 to 2.1.1 (#841) 2020-05-24 20:53:59 +02:00
699fbff082 Fix debug channel (#846) 2020-05-24 20:53:23 +02:00
4c295f2ab4 Bump firebase-messaging from 20.1.7 to 20.2.0 (#839) 2020-05-24 18:42:24 +00:00
dcbaa170db Bump about_libraries from 8.1.2 to 8.1.3 (#845) 2020-05-24 20:36:09 +02:00
c71b533645 Bump firebase-inappmessaging-display-ktx from 19.0.6 to 19.0.7 (#838) 2020-05-24 18:10:47 +00:00
63f2576ff1 Hide advanced login options button (#837) 2020-05-24 20:07:24 +02:00
b744a4182b Open dontkillmyapp.com if no action found in app killer manager (#835) 2020-05-24 20:04:09 +02:00
0c4364609b Show check for updates dialog before report a bug (#834) 2020-05-24 20:03:46 +02:00
3308d7fe6f Wrap long preference titles (#836) 2020-05-24 19:52:01 +02:00
2cdde78c54 Allow access to saturday in timetable and attendance (#833) 2020-05-24 19:48:56 +02:00
428b599be0 Improve firebase logging (#832) 2020-05-24 19:48:14 +02:00
3541ab81b8 Destroy webview in password recover before setting binding to null (#829) 2020-05-24 19:47:20 +02:00
7fa14e5077 Set app_name in build.gradle (#830) 2020-05-24 19:36:40 +02:00
cec1068f2e Fix crash in timetable time left (#826) 2020-05-24 19:34:10 +02:00
f737018548 Add debug statements to get/update methods in grade details adapter (#827) 2020-05-24 19:33:04 +02:00
9c01316178 Fix mark message as read in search mode (#828) 2020-05-24 19:24:01 +02:00
c3a6f8253a Fix grade sorting (#825) 2020-05-24 19:23:35 +02:00
d558c4db66 New Crowdin translations (#817) 2020-05-23 16:34:26 +02:00
722886aaf2 Merge tag '0.18.0' into develop
Version 0.18.0
2020-05-21 00:59:19 +02:00
31902a7667 Merge branch 'release/0.18.0' 2020-05-21 00:59:13 +02:00
4c1c4f8a43 Version 0.18.0 2020-05-21 00:59:05 +02:00
7850412ba9 Fix crash in timetable on api < 21 (#816) 2020-05-20 23:08:32 +02:00
4f0ff5f49c New Crowdin translations (#813) 2020-05-20 22:48:09 +02:00
131ba7dbb1 Add app killer manager to settings (#808) 2020-05-20 16:59:26 +02:00
b95b529015 Add lesson time left display (#550) 2020-05-20 16:06:24 +02:00
29226dd93e Add notification about upcoming lesson (#578) 2020-05-20 15:11:01 +02:00
115da64167 Add search in messages (#804) 2020-05-20 14:12:32 +02:00
6cd1877af7 Fix notifications on android 8.0 (#814) 2020-05-20 12:34:29 +02:00
78a90591fd Bump coil from 0.10.1 to 0.11.0 (#812) 2020-05-16 21:06:45 +00:00
45265d025d Bump appcompat from 1.2.0-beta01 to 1.2.0-rc01 (#811) 2020-05-16 20:52:37 +00:00
9bf5c2dc40 Bump firebase-crashlytics-gradle from 2.0.0 to 2.1.0 (#810) 2020-05-16 20:48:15 +00:00
ee4bdd2a9a Bump firebase-analytics from 17.4.0 to 17.4.1 (#809) 2020-05-16 20:46:11 +00:00
0b75635ad5 Add option to hide/show chart list in grade class stats (#807) 2020-05-16 22:21:14 +02:00
f7b5b9c413 Add fullscreen mode to homework dialog (#806) 2020-05-16 22:06:00 +02:00
52d66ac30b New Crowdin translations (#797) 2020-05-13 11:25:50 +02:00
6ac5c6a0b4 Add widget system theme option (#759) 2020-05-10 12:00:21 +02:00
45fc76a9a5 Update translations (#794) 2020-05-10 11:34:54 +02:00
6d1fa0cf05 Optimize grade average provider (#792) 2020-05-10 10:51:01 +02:00
8eb0c0351b Use view binding instead of kotlin synthetics (#791) 2020-05-10 10:39:10 +02:00
ec80f939f1 Update Crowdin configuration file 2020-05-06 23:10:19 +02:00
70fc51a0b5 Update Crowdin configuration file 2020-05-06 23:07:13 +02:00
bd700a88bf Add nav bar color in night style (#790) 2020-05-03 15:06:49 +02:00
98f2f0e74f Migrate from fabric to firebase crashlytics (#789) 2020-05-01 19:00:42 +02:00
4a3b746d48 Remove flexible adapter (#781) 2020-05-01 17:38:19 +02:00
a1f864b35e Add importantForAutofill to login fields (#788) 2020-05-01 12:54:01 +02:00
17ac3cfd52 Bump firebase-analytics from 17.3.0 to 17.4.0 (#787) 2020-04-30 22:40:38 +00:00
c6c2b1c6a3 Bump coil from 0.9.5 to 0.10.1 (#785) 2020-04-29 14:40:12 +00:00
5fba3d5775 Bump firebase-inappmessaging-ktx from 19.0.5 to 19.0.6 (#782) 2020-04-28 21:25:27 +00:00
6fe62edd63 Bump firebase-inappmessaging-display-ktx from 19.0.5 to 19.0.6 (#786) 2020-04-28 21:04:54 +00:00
87af3da1ad Bump threetenabp from 1.2.3 to 1.2.4 (#783) 2020-04-28 20:57:14 +00:00
155f0cc347 Bump threetenbp from 1.4.3 to 1.4.4 (#784) 2020-04-28 20:56:55 +00:00
2de1ad5334 Merge tag '0.17.4' into develop
Version 0.17.4
2020-04-23 19:24:59 +02:00
302 changed files with 5671 additions and 5091 deletions

View File

@ -1,3 +1,3 @@
component_depth: 8
component_depth: 10
languages:
- kotlin

View File

@ -14,7 +14,7 @@ cache:
branches:
only:
- develop
- 0.17.4
- 0.18.3
android:
licenses:
@ -48,20 +48,15 @@ before_script:
script:
- ./gradlew dependencies --stacktrace --daemon
- fossa --no-ansi || true
#- ./gradlew lintPlayRelease -x fabricGenerateResourcesPlayRelease --stacktrace --daemon
- ./gradlew -Pcoverage testPlayDebugUnitTest -x fabricGenerateResourcesPlay --stacktrace --daemon
- ./gradlew -Pcoverage testPlayDebugUnitTest --stacktrace --daemon
- ./gradlew -Pcoverage createFdroidDebugCoverageReport --stacktrace --daemon
- ./gradlew -Pcoverage jacocoTestReport --stacktrace --daemon
- if [ -z ${SONAR_HOST+x} ]; then echo "sonar scan skipped"; else
git fetch --unshallow;
./gradlew sonarqube -x test -x lint -x fabricGenerateResourcesPlayRelease -x fabricGenerateResourcesFdroidRelease -Dsonar.host.url=$SONAR_HOST -Dsonar.organization=$SONAR_ORG -Dsonar.login=$SONAR_KEY -Dsonar.branch.name=${TRAVIS_PULL_REQUEST_BRANCH:-$TRAVIS_BRANCH} --stacktrace --daemon;
fi
- |
if [ $TRAVIS_TAG ]; then
gpg --yes --batch --passphrase=$SERVICES_ENCRYPT_KEY ./app/src/release/google-services.json.gpg;
gpg --yes --batch --passphrase=$ENCRYPT_KEY ./app/key.p12.gpg;
gpg --yes --batch --passphrase=$ENCRYPT_KEY ./app/upload-key.jks.gpg;
./gradlew publishPlayRelease -PenableCrashlytics --stacktrace;
./gradlew publishPlayRelease -PenableFirebase --stacktrace;
fi
after_success:

View File

@ -1,8 +1,7 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'io.fabric'
apply plugin: 'com.google.firebase.crashlytics'
apply plugin: 'com.github.triplet.play'
apply plugin: 'com.mikepenz.aboutlibraries.plugin'
apply from: 'jacoco.gradle'
@ -18,14 +17,14 @@ android {
testApplicationId "io.github.tests.wulkanowy"
minSdkVersion 17
targetSdkVersion 29
versionCode 57
versionName "0.17.4"
versionCode 62
versionName "0.18.3"
multiDexEnabled true
resValue "string", "app_name", "Wulkanowy"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true
manifestPlaceholders = [
fabric_api_key : System.getenv("FABRIC_API_KEY") ?: "null",
crashlytics_enabled: project.hasProperty("enableCrashlytics")
firebase_enabled: project.hasProperty("enableFirebase")
]
javaCompileOptions {
annotationProcessorOptions {
@ -52,18 +51,17 @@ android {
buildTypes {
release {
buildConfigField "boolean", "CRASHLYTICS_ENABLED", "true"
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
}
debug {
buildConfigField "boolean", "CRASHLYTICS_ENABLED", project.hasProperty("enableCrashlytics") ? "true" : "false"
resValue "string", "app_name", "Wulkanowy DEV " + defaultConfig.versionCode
applicationIdSuffix ".dev"
versionNameSuffix "-dev"
testCoverageEnabled = project.hasProperty('coverage')
ext.enableCrashlytics = project.hasProperty("enableCrashlytics")
ext.enableCrashlytics = project.hasProperty("enableFirebase")
}
}
@ -75,11 +73,14 @@ android {
}
fdroid {
buildConfigField "boolean", "CRASHLYTICS_ENABLED", "false"
dimension "platform"
}
}
viewBinding {
enabled = true
}
lintOptions {
disable 'HardwareIds'
}
@ -103,10 +104,6 @@ android {
}
}
androidExtensions {
experimental = true
}
play {
serviceAccountEmail = System.getenv("PLAY_SERVICE_ACCOUNT_EMAIL") ?: "jan@fakelog.cf"
serviceAccountCredentials = file('key.p12')
@ -117,8 +114,7 @@ play {
ext {
work_manager = "2.3.4"
room = "2.2.5"
dagger = "2.27"
// don't update https://github.com/ChuckerTeam/chucker/issues/242
dagger = "2.28"
chucker = "3.2.0"
mockk = "1.9.2"
}
@ -128,12 +124,12 @@ configurations.all {
}
dependencies {
implementation "io.github.wulkanowy:sdk:0.17.4"
implementation "io.github.wulkanowy:sdk:0.18.3"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation "androidx.core:core-ktx:1.2.0"
implementation "androidx.activity:activity-ktx:1.1.0"
implementation "androidx.appcompat:appcompat:1.2.0-beta01"
implementation "androidx.appcompat:appcompat:1.2.0-rc01"
implementation "androidx.appcompat:appcompat-resources:1.1.0"
implementation "androidx.fragment:fragment-ktx:1.2.4"
implementation "androidx.annotation:annotation:1.1.0"
@ -167,30 +163,29 @@ dependencies {
implementation "com.squareup.inject:assisted-inject-annotations-dagger2:0.5.2"
kapt "com.squareup.inject:assisted-inject-processor-dagger2:0.5.2"
implementation "eu.davidea:flexible-adapter:5.1.0"
implementation "eu.davidea:flexible-adapter-ui:1.0.0"
implementation "com.aurelhubert:ahbottomnavigation:2.3.4"
implementation "com.ncapdevi:frag-nav:3.3.0"
implementation "com.github.YarikSOffice:lingver:1.2.1"
implementation "com.github.YarikSOffice:lingver:1.2.2"
implementation "com.github.pwittchen:reactivenetwork-rx2:3.0.6"
implementation "com.github.pwittchen:reactivenetwork-rx2:3.0.8"
implementation "io.reactivex.rxjava2:rxandroid:2.1.1"
implementation "io.reactivex.rxjava2:rxjava:2.2.19"
implementation "com.google.code.gson:gson:2.8.6"
implementation "com.jakewharton.threetenabp:threetenabp:1.2.3"
implementation "com.jakewharton.threetenabp:threetenabp:1.2.4"
implementation "com.jakewharton.timber:timber:4.7.1"
implementation "at.favre.lib:slf4j-timber:1.0.1"
implementation "fr.bipi.treessence:treessence:0.3.2"
implementation "com.mikepenz:aboutlibraries-core:$about_libraries"
implementation 'com.wdullaer:materialdatetimepicker:4.2.3'
implementation "io.coil-kt:coil:0.9.5"
implementation "io.coil-kt:coil:0.11.0"
implementation "io.github.wulkanowy:AppKillerManager:3.0.0"
playImplementation 'com.google.firebase:firebase-analytics:17.3.0'
playImplementation 'com.google.firebase:firebase-inappmessaging-display-ktx:19.0.5'
playImplementation "com.google.firebase:firebase-inappmessaging-ktx:19.0.5"
playImplementation "com.google.firebase:firebase-messaging:20.1.0"
playImplementation "com.crashlytics.sdk.android:crashlytics:2.10.1"
playImplementation 'com.google.firebase:firebase-analytics:17.4.2'
playImplementation 'com.google.firebase:firebase-inappmessaging-display-ktx:19.0.7'
playImplementation "com.google.firebase:firebase-inappmessaging-ktx:19.0.7"
playImplementation 'com.google.firebase:firebase-messaging:20.2.0'
playImplementation 'com.google.firebase:firebase-crashlytics:17.0.0'
playImplementation 'com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava'
releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker"
@ -200,7 +195,7 @@ dependencies {
testImplementation "junit:junit:4.13"
testImplementation "io.mockk:mockk:$mockk"
testImplementation "org.threeten:threetenbp:1.4.3"
testImplementation "org.threeten:threetenbp:1.4.4"
testImplementation "org.mockito:mockito-inline:3.3.3"
androidTestImplementation "androidx.test:core:1.2.0"

View File

@ -24,7 +24,7 @@ class GradeLocalTest {
fun createDb() {
testDb = Room.inMemoryDatabaseBuilder(ApplicationProvider.getApplicationContext(), AppDatabase::class.java)
.build()
gradeLocal = GradeLocal(testDb.gradeDao)
gradeLocal = GradeLocal(testDb.gradeDao, testDb.gradeSummaryDao)
}
@After
@ -43,7 +43,7 @@ class GradeLocalTest {
val semester = Semester(1, 2, "", 2019, 2, 1, now(), now(), 1, 1)
val grades = gradeLocal
.getGrades(semester)
.getGradesDetails(semester)
.blockingGet()
assertEquals(2, grades.size)

View File

@ -11,6 +11,7 @@ import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.repositories.TestInternetObservingStrategy
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.sdk.pojo.Grade
import io.mockk.MockKAnnotations
import io.mockk.every
import io.mockk.impl.annotations.MockK
@ -52,7 +53,7 @@ class GradeRepositoryTest {
fun initApi() {
MockKAnnotations.init(this)
testDb = Room.inMemoryDatabaseBuilder(getApplicationContext(), AppDatabase::class.java).build()
gradeLocal = GradeLocal(testDb.gradeDao)
gradeLocal = GradeLocal(testDb.gradeDao, testDb.gradeSummaryDao)
gradeRemote = GradeRemote(mockSdk)
every { studentMock.registrationDate } returns LocalDateTime.of(2019, 2, 27, 12, 0)
@ -75,10 +76,10 @@ class GradeRepositoryTest {
createGradeApi(5, 4.0, of(2019, 2, 26), "przed zalogowanie w aplikacji"),
createGradeApi(5, 4.0, of(2019, 2, 27), "Ocena z dnia logowania"),
createGradeApi(5, 4.0, of(2019, 2, 28), "Ocena jeszcze nowsza")
))
) to emptyList())
val grades = GradeRepository(settings, gradeLocal, gradeRemote)
.getGrades(studentMock, semesterMock, true).blockingGet().sortedByDescending { it.date }
.getGrades(studentMock, semesterMock, true).blockingGet().first.sortedByDescending { it.date }
assertFalse { grades[0].isRead }
assertFalse { grades[1].isRead }
@ -99,10 +100,10 @@ class GradeRepositoryTest {
createGradeApi(4, 3.0, of(2019, 2, 26), "starszą niż ostatnia lokalnie"),
createGradeApi(3, 4.0, of(2019, 2, 27), "Ta jest z tego samego dnia co ostatnia lokalnie"),
createGradeApi(2, 5.0, of(2019, 2, 28), "Ta jest już w ogóle nowa")
))
) to emptyList())
val grades = GradeRepository(settings, gradeLocal, gradeRemote)
.getGrades(studentMock, semesterMock, true).blockingGet().sortedByDescending { it.date }
.getGrades(studentMock, semesterMock, true).blockingGet().first.sortedByDescending { it.date }
assertFalse { grades[0].isRead }
assertFalse { grades[1].isRead }
@ -121,12 +122,12 @@ class GradeRepositoryTest {
every { mockSdk.getGrades(1) } returns Single.just(listOf(
createGradeApi(5, 3.0, of(2019, 2, 25), "Taka sama ocena"),
createGradeApi(3, 5.0, of(2019, 2, 26), "Jakaś inna ocena")
))
) to emptyList())
val grades = GradeRepository(settings, gradeLocal, gradeRemote)
.getGrades(studentMock, semesterMock, true).blockingGet()
assertEquals(2, grades.size)
assertEquals(2, grades.first.size)
}
@Test
@ -140,12 +141,12 @@ class GradeRepositoryTest {
createGradeApi(5, 3.0, of(2019, 2, 25), "Taka sama ocena"),
createGradeApi(5, 3.0, of(2019, 2, 25), "Taka sama ocena"),
createGradeApi(3, 5.0, of(2019, 2, 26), "Jakaś inna ocena")
))
) to emptyList())
val grades = GradeRepository(settings, gradeLocal, gradeRemote)
.getGrades(studentMock, semesterMock, true).blockingGet()
assertEquals(3, grades.size)
assertEquals(3, grades.first.size)
}
@Test
@ -156,12 +157,12 @@ class GradeRepositoryTest {
createGradeApi(5, 3.0, of(2019, 2, 25), "Taka sama ocena"),
createGradeApi(5, 3.0, of(2019, 2, 25), "Taka sama ocena"),
createGradeApi(3, 5.0, of(2019, 2, 26), "Jakaś inna ocena")
))
) to emptyList())
val grades = GradeRepository(settings, gradeLocal, gradeRemote)
.getGrades(studentMock, semesterMock, true).blockingGet()
assertEquals(3, grades.size)
assertEquals(3, grades.first.size)
}
@Test
@ -171,11 +172,11 @@ class GradeRepositoryTest {
createGradeLocal(3, 5.0, of(2019, 2, 26), "Jakaś inna ocena")
))
every { mockSdk.getGrades(1) } returns Single.just(listOf())
every { mockSdk.getGrades(1) } returns Single.just(emptyList<Grade>() to emptyList())
val grades = GradeRepository(settings, gradeLocal, gradeRemote)
.getGrades(studentMock, semesterMock, true).blockingGet()
assertEquals(0, grades.size)
assertEquals(0, grades.first.size)
}
}

View File

@ -8,12 +8,15 @@ import androidx.test.filters.SdkSuppress
import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.InternetObservingSettings
import io.github.wulkanowy.data.db.AppDatabase
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.repositories.TestInternetObservingStrategy
import io.github.wulkanowy.data.repositories.getStudent
import io.github.wulkanowy.services.alarm.TimetableNotificationSchedulerHelper
import io.github.wulkanowy.sdk.Sdk
import io.mockk.MockKAnnotations
import io.mockk.every
import io.mockk.impl.annotations.MockK
import io.mockk.mockk
import io.reactivex.Single
import org.junit.After
import org.junit.Before
@ -34,11 +37,17 @@ class TimetableRepositoryTest {
.strategy(TestInternetObservingStrategy())
.build()
@MockK
private lateinit var studentMock: Student
private val student = getStudent()
@MockK
private lateinit var semesterMock: Semester
@MockK
private lateinit var timetableNotificationSchedulerHelper: TimetableNotificationSchedulerHelper
private lateinit var timetableRemote: TimetableRemote
private lateinit var timetableLocal: TimetableLocal
@ -52,10 +61,17 @@ class TimetableRepositoryTest {
timetableLocal = TimetableLocal(testDb.timetableDao)
timetableRemote = TimetableRemote(mockSdk)
every { timetableNotificationSchedulerHelper.scheduleNotifications(any(), any()) } returns mockk()
every { timetableNotificationSchedulerHelper.cancelScheduled(any(), any()) } returns mockk()
every { studentMock.studentId } returns 1
every { studentMock.studentName } returns "Jan Kowalski"
every { semesterMock.studentId } returns 1
every { semesterMock.diaryId } returns 2
every { semesterMock.schoolYear } returns 2019
every { semesterMock.semesterId } returns 1
every { mockSdk.switchDiary(any(), any()) } returns mockSdk
}
@ -80,7 +96,7 @@ class TimetableRepositoryTest {
createTimetableRemote(of(2019, 3, 5, 10, 30), 4, "", "W-F")
))
val lessons = TimetableRepository(settings, timetableLocal, timetableRemote)
val lessons = TimetableRepository(settings, timetableLocal, timetableRemote, timetableNotificationSchedulerHelper)
.getTimetable(student, semesterMock, LocalDate.of(2019, 3, 5), LocalDate.of(2019, 3, 5), true)
.blockingGet()
@ -126,7 +142,7 @@ class TimetableRepositoryTest {
createTimetableRemote(of(2019, 12, 25, 10, 40), 4, "126", "Matematyka", "Paweł Czwartkowski", true)
))
val lessons = TimetableRepository(settings, timetableLocal, timetableRemote)
val lessons = TimetableRepository(settings, timetableLocal, timetableRemote, timetableNotificationSchedulerHelper)
.getTimetable(student, semesterMock, LocalDate.of(2019, 12, 23), LocalDate.of(2019, 12, 25), true)
.blockingGet()

View File

@ -1,3 +0,0 @@
<resources>
<string name="app_name">Wulkanowy DEV</string>
</resources>

View File

@ -5,8 +5,6 @@ package io.github.wulkanowy.utils
import android.content.Context
import timber.log.Timber
fun initCrashlytics(context: Context, appInfo: AppInfo) {}
open class TimberTreeNoOp : Timber.Tree() {
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {}
}

View File

@ -1,13 +1,18 @@
package io.github.wulkanowy.utils
import android.app.Activity
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
@Suppress("UNUSED_PARAMETER")
class FirebaseAnalyticsHelper @Inject constructor() {
@Suppress("UNUSED_PARAMETER")
fun logEvent(name: String, vararg params: Pair<String, Any?>) {
// do nothing
}
fun setCurrentScreen(activity: Activity, name: String?) {
// do nothing
}
}

View File

@ -18,7 +18,8 @@
android:supportsRtl="false"
android:theme="@style/WulkanowyTheme"
android:usesCleartextTraffic="true"
tools:ignore="GoogleAppIndexingWarning,UnusedAttribute">
tools:ignore="GoogleAppIndexingWarning,UnusedAttribute"
tools:replace="android:supportsRtl,android:allowBackup">
<activity
android:name=".ui.modules.splash.SplashActivity"
android:screenOrientation="portrait"
@ -39,7 +40,8 @@
android:name=".ui.modules.main.MainActivity"
android:configChanges="orientation|screenSize"
android:label="@string/main_title"
android:theme="@style/WulkanowyTheme.NoActionBar" />
android:theme="@style/WulkanowyTheme.NoActionBar"
android:windowSoftInputMode="adjustPan" />
<activity
android:name=".ui.modules.message.send.SendMessageActivity"
android:configChanges="orientation|screenSize"
@ -91,6 +93,8 @@
android:resource="@xml/provider_widget_lucky_number" />
</receiver>
<receiver android:name=".services.alarm.TimetableNotificationReceiver" />
<provider
android:name="androidx.work.impl.WorkManagerInitializer"
android:authorities="${applicationId}.workmanager-init"
@ -107,12 +111,33 @@
android:resource="@xml/provider_paths" />
</provider>
<!-- workaround for https://github.com/firebase/firebase-android-sdk/issues/473 enabled:false -->
<!-- https://firebase.googleblog.com/2017/03/take-control-of-your-firebase-init-on.html -->
<provider
android:name="com.google.firebase.provider.FirebaseInitProvider"
android:authorities="${applicationId}.firebaseinitprovider"
android:enabled="${firebase_enabled}"
android:exported="false" />
<meta-data
android:name="io.fabric.ApiKey"
android:value="${fabric_api_key}" />
android:name="firebase_analytics_collection_enabled"
android:value="${firebase_enabled}" />
<meta-data
android:name="google_analytics_adid_collection_enabled"
android:value="${firebase_enabled}" />
<meta-data
android:name="firebase_crashlytics_collection_enabled"
android:value="${crashlytics_enabled}" />
android:value="${firebase_enabled}" />
<meta-data
android:name="firebase_messaging_auto_init_enabled"
android:value="${firebase_enabled}" />
<meta-data
android:name="firebase_inapp_messaging_auto_data_collection_enabled"
android:value="${firebase_enabled}" />
<meta-data
android:name="com.google.firebase.messaging.default_notification_icon"

View File

@ -10,8 +10,6 @@ import com.jakewharton.threetenabp.AndroidThreeTen
import com.yariksoffice.lingver.Lingver
import dagger.android.AndroidInjector
import dagger.android.support.DaggerApplication
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.utils.Log
import fr.bipi.tressence.file.FileLoggerTree
import io.github.wulkanowy.di.DaggerAppComponent
import io.github.wulkanowy.services.sync.SyncWorkerFactory
@ -21,7 +19,6 @@ import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.CrashlyticsExceptionTree
import io.github.wulkanowy.utils.CrashlyticsTree
import io.github.wulkanowy.utils.DebugLogTree
import io.github.wulkanowy.utils.initCrashlytics
import io.reactivex.exceptions.UndeliverableException
import io.reactivex.plugins.RxJavaPlugins
import timber.log.Timber
@ -52,12 +49,10 @@ class WulkanowyApp : DaggerApplication(), Configuration.Provider {
themeManager.applyDefaultTheme()
initLogging()
initCrashlytics(this, appInfo)
}
private fun initLogging() {
if (appInfo.isDebug) {
FlexibleAdapter.enableLogs(Log.Level.DEBUG)
Timber.plant(DebugLogTree())
Timber.plant(FileLoggerTree.Builder()
.withFileName("wulkanowy.%g.log")

View File

@ -1,9 +1,11 @@
package io.github.wulkanowy.data
import android.app.AlarmManager
import android.content.Context
import android.content.SharedPreferences
import android.content.res.AssetManager
import android.content.res.Resources
import androidx.core.content.getSystemService
import androidx.preference.PreferenceManager
import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.InternetObservingSettings
import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.strategy.WalledGardenInternetObservingStrategy

View File

@ -1,3 +0,0 @@
package io.github.wulkanowy.data.pojos
class AppCreator(val displayName: String, val githubUsername: String)

View File

@ -0,0 +1,3 @@
package io.github.wulkanowy.data.pojos
class Contributor(val displayName: String, val githubUsername: String)

View File

@ -2,18 +2,18 @@ package io.github.wulkanowy.data.repositories.appcreator
import android.content.res.AssetManager
import com.google.gson.Gson
import io.github.wulkanowy.data.pojos.AppCreator
import io.github.wulkanowy.data.pojos.Contributor
import io.reactivex.Single
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class AppCreatorRepository @Inject constructor(private val assets: AssetManager) {
fun getAppCreators(): Single<List<AppCreator>> {
return Single.fromCallable<List<AppCreator>> {
fun getAppCreators(): Single<List<Contributor>> {
return Single.fromCallable<List<Contributor>> {
Gson().fromJson(
assets.open("contributors.json").bufferedReader().use { it.readText() },
Array<AppCreator>::class.java
Array<Contributor>::class.java
).toList()
}
}

View File

@ -5,7 +5,7 @@ import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.Inter
import io.github.wulkanowy.data.db.entities.Attendance
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.utils.friday
import io.github.wulkanowy.utils.sunday
import io.github.wulkanowy.utils.monday
import io.github.wulkanowy.utils.uniqueSubtract
import io.reactivex.Single
@ -22,19 +22,19 @@ class AttendanceRepository @Inject constructor(
) {
fun getAttendance(student: Student, semester: Semester, start: LocalDate, end: LocalDate, forceRefresh: Boolean): Single<List<Attendance>> {
return local.getAttendance(semester, start.monday, end.friday).filter { !forceRefresh }
return local.getAttendance(semester, start.monday, end.sunday).filter { !forceRefresh }
.switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings).flatMap {
if (it) remote.getAttendance(student, semester, start.monday, end.friday)
if (it) remote.getAttendance(student, semester, start.monday, end.sunday)
else Single.error(UnknownHostException())
}.flatMap { newAttendance ->
local.getAttendance(semester, start.monday, end.friday)
local.getAttendance(semester, start.monday, end.sunday)
.toSingle(emptyList())
.doOnSuccess { oldAttendance ->
local.deleteAttendance(oldAttendance.uniqueSubtract(newAttendance))
local.saveAttendance(newAttendance.uniqueSubtract(oldAttendance))
}
}.flatMap {
local.getAttendance(semester, start.monday, end.friday)
local.getAttendance(semester, start.monday, end.sunday)
.toSingle(emptyList())
}).map { list -> list.filter { it.date in start..end } }
}

View File

@ -5,7 +5,7 @@ import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.Inter
import io.github.wulkanowy.data.db.entities.CompletedLesson
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.utils.friday
import io.github.wulkanowy.utils.sunday
import io.github.wulkanowy.utils.monday
import io.github.wulkanowy.utils.uniqueSubtract
import io.reactivex.Single
@ -22,20 +22,20 @@ class CompletedLessonsRepository @Inject constructor(
) {
fun getCompletedLessons(student: Student, semester: Semester, start: LocalDate, end: LocalDate, forceRefresh: Boolean = false): Single<List<CompletedLesson>> {
return local.getCompletedLessons(semester, start.monday, end.friday).filter { !forceRefresh }
return local.getCompletedLessons(semester, start.monday, end.sunday).filter { !forceRefresh }
.switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings)
.flatMap {
if (it) remote.getCompletedLessons(student, semester, start.monday, end.friday)
if (it) remote.getCompletedLessons(student, semester, start.monday, end.sunday)
else Single.error(UnknownHostException())
}.flatMap { new ->
local.getCompletedLessons(semester, start.monday, end.friday)
local.getCompletedLessons(semester, start.monday, end.sunday)
.toSingle(emptyList())
.doOnSuccess { old ->
local.deleteCompleteLessons(old.uniqueSubtract(new))
local.saveCompletedLessons(new.uniqueSubtract(old))
}
}.flatMap {
local.getCompletedLessons(semester, start.monday, end.friday)
local.getCompletedLessons(semester, start.monday, end.sunday)
.toSingle(emptyList())
}).map { list -> list.filter { it.date in start..end } }
}

View File

@ -5,7 +5,7 @@ import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.Inter
import io.github.wulkanowy.data.db.entities.Exam
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.utils.friday
import io.github.wulkanowy.utils.sunday
import io.github.wulkanowy.utils.monday
import io.github.wulkanowy.utils.uniqueSubtract
import io.reactivex.Single
@ -22,20 +22,20 @@ class ExamRepository @Inject constructor(
) {
fun getExams(student: Student, semester: Semester, start: LocalDate, end: LocalDate, forceRefresh: Boolean = false): Single<List<Exam>> {
return local.getExams(semester, start.monday, end.friday).filter { !forceRefresh }
return local.getExams(semester, start.monday, end.sunday).filter { !forceRefresh }
.switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings)
.flatMap {
if (it) remote.getExams(student, semester, start.monday, end.friday)
if (it) remote.getExams(student, semester, start.monday, end.sunday)
else Single.error(UnknownHostException())
}.flatMap { new ->
local.getExams(semester, start.monday, end.friday)
local.getExams(semester, start.monday, end.sunday)
.toSingle(emptyList())
.doOnSuccess { old ->
local.deleteExams(old.uniqueSubtract(new))
local.saveExams(new.uniqueSubtract(old))
}
}.flatMap {
local.getExams(semester, start.monday, end.friday)
local.getExams(semester, start.monday, end.sunday)
.toSingle(emptyList())
}).map { list -> list.filter { it.date in start..end } }
}

View File

@ -1,14 +1,19 @@
package io.github.wulkanowy.data.repositories.grade
import io.github.wulkanowy.data.db.dao.GradeDao
import io.github.wulkanowy.data.db.dao.GradeSummaryDao
import io.github.wulkanowy.data.db.entities.Grade
import io.github.wulkanowy.data.db.entities.GradeSummary
import io.github.wulkanowy.data.db.entities.Semester
import io.reactivex.Maybe
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class GradeLocal @Inject constructor(private val gradeDb: GradeDao) {
class GradeLocal @Inject constructor(
private val gradeDb: GradeDao,
private val gradeSummaryDb: GradeSummaryDao
) {
fun saveGrades(grades: List<Grade>) {
gradeDb.insertAll(grades)
@ -22,7 +27,19 @@ class GradeLocal @Inject constructor(private val gradeDb: GradeDao) {
gradeDb.updateAll(grades)
}
fun getGrades(semester: Semester): Maybe<List<Grade>> {
fun getGradesDetails(semester: Semester): Maybe<List<Grade>> {
return gradeDb.loadAll(semester.semesterId, semester.studentId).filter { it.isNotEmpty() }
}
fun saveGradesSummary(gradesSummary: List<GradeSummary>) {
gradeSummaryDb.insertAll(gradesSummary)
}
fun deleteGradesSummary(gradesSummary: List<GradeSummary>) {
gradeSummaryDb.deleteAll(gradesSummary)
}
fun getGradesSummary(semester: Semester): Maybe<List<GradeSummary>> {
return gradeSummaryDb.loadAll(semester.semesterId, semester.studentId).filter { it.isNotEmpty() }
}
}

View File

@ -1,6 +1,7 @@
package io.github.wulkanowy.data.repositories.grade
import io.github.wulkanowy.data.db.entities.Grade
import io.github.wulkanowy.data.db.entities.GradeSummary
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.sdk.Sdk
@ -12,11 +13,11 @@ import javax.inject.Singleton
@Singleton
class GradeRemote @Inject constructor(private val sdk: Sdk) {
fun getGrades(student: Student, semester: Semester): Single<List<Grade>> {
fun getGrades(student: Student, semester: Semester): Single<Pair<List<Grade>, List<GradeSummary>>> {
return sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear)
.getGrades(semester.semesterId)
.map { grades ->
grades.map {
.map { (details, summary) ->
details.map {
Grade(
studentId = semester.studentId,
semesterId = semester.semesterId,
@ -33,6 +34,19 @@ class GradeRemote @Inject constructor(private val sdk: Sdk) {
date = it.date,
teacher = it.teacher
)
} to summary.map {
GradeSummary(
semesterId = semester.semesterId,
studentId = semester.studentId,
position = 0,
subject = it.name,
predictedGrade = it.predicted,
finalGrade = it.final,
pointsSum = it.pointsSum,
proposedPoints = it.proposedPoints,
finalPoints = it.finalPoints,
average = it.average
)
}
}
}

View File

@ -3,6 +3,7 @@ package io.github.wulkanowy.data.repositories.grade
import com.github.pwittchen.reactivenetwork.library.rx2.ReactiveNetwork
import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.InternetObservingSettings
import io.github.wulkanowy.data.db.entities.Grade
import io.github.wulkanowy.data.db.entities.GradeSummary
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.utils.uniqueSubtract
@ -19,34 +20,47 @@ class GradeRepository @Inject constructor(
private val remote: GradeRemote
) {
fun getGrades(student: Student, semester: Semester, forceRefresh: Boolean = false, notify: Boolean = false): Single<List<Grade>> {
return local.getGrades(semester).filter { !forceRefresh }
.switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings)
.flatMap {
if (it) remote.getGrades(student, semester)
else Single.error(UnknownHostException())
}.flatMap { new ->
local.getGrades(semester).toSingle(emptyList())
.doOnSuccess { old ->
val notifyBreakDate = old.maxBy { it.date }?.date ?: student.registrationDate.toLocalDate()
local.deleteGrades(old.uniqueSubtract(new))
local.saveGrades(new.uniqueSubtract(old)
.onEach {
if (it.date >= notifyBreakDate) it.apply {
isRead = false
if (notify) isNotified = false
}
})
}
}.flatMap { local.getGrades(semester).toSingle(emptyList()) })
fun getGrades(student: Student, semester: Semester, forceRefresh: Boolean = false, notify: Boolean = false): Single<Pair<List<Grade>, List<GradeSummary>>> {
return local.getGradesDetails(semester).flatMap { details ->
local.getGradesSummary(semester).map { summary -> details to summary }
}.filter { !forceRefresh }
.switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings).flatMap {
if (it) remote.getGrades(student, semester)
else Single.error(UnknownHostException())
}.flatMap { (newDetails, newSummary) ->
local.getGradesDetails(semester).toSingle(emptyList())
.doOnSuccess { old ->
val notifyBreakDate = old.maxBy { it.date }?.date ?: student.registrationDate.toLocalDate()
local.deleteGrades(old.uniqueSubtract(newDetails))
local.saveGrades(newDetails.uniqueSubtract(old)
.onEach {
if (it.date >= notifyBreakDate) it.apply {
isRead = false
if (notify) isNotified = false
}
})
}.flatMap {
local.getGradesSummary(semester).toSingle(emptyList())
.doOnSuccess { old ->
local.deleteGradesSummary(old.uniqueSubtract(newSummary))
local.saveGradesSummary(newSummary.uniqueSubtract(old))
}
}
}.flatMap {
local.getGradesDetails(semester).toSingle(emptyList()).flatMap { details ->
local.getGradesSummary(semester).toSingle(emptyList()).map { summary ->
details to summary
}
}
})
}
fun getUnreadGrades(semester: Semester): Single<List<Grade>> {
return local.getGrades(semester).map { it.filter { grade -> !grade.isRead } }.toSingle(emptyList())
return local.getGradesDetails(semester).map { it.filter { grade -> !grade.isRead } }.toSingle(emptyList())
}
fun getNotNotifiedGrades(semester: Semester): Single<List<Grade>> {
return local.getGrades(semester).map { it.filter { grade -> !grade.isNotified } }.toSingle(emptyList())
return local.getGradesDetails(semester).map { it.filter { grade -> !grade.isNotified } }.toSingle(emptyList())
}
fun updateGrade(grade: Grade): Completable {

View File

@ -1,24 +0,0 @@
package io.github.wulkanowy.data.repositories.gradessummary
import io.github.wulkanowy.data.db.dao.GradeSummaryDao
import io.github.wulkanowy.data.db.entities.GradeSummary
import io.github.wulkanowy.data.db.entities.Semester
import io.reactivex.Maybe
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class GradeSummaryLocal @Inject constructor(private val gradeSummaryDb: GradeSummaryDao) {
fun saveGradesSummary(gradesSummary: List<GradeSummary>) {
gradeSummaryDb.insertAll(gradesSummary)
}
fun deleteGradesSummary(gradesSummary: List<GradeSummary>) {
gradeSummaryDb.deleteAll(gradesSummary)
}
fun getGradesSummary(semester: Semester): Maybe<List<GradeSummary>> {
return gradeSummaryDb.loadAll(semester.semesterId, semester.studentId).filter { it.isNotEmpty() }
}
}

View File

@ -1,35 +0,0 @@
package io.github.wulkanowy.data.repositories.gradessummary
import io.github.wulkanowy.data.db.entities.GradeSummary
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.init
import io.reactivex.Single
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class GradeSummaryRemote @Inject constructor(private val sdk: Sdk) {
fun getGradeSummary(student: Student, semester: Semester): Single<List<GradeSummary>> {
return sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear)
.getGradesSummary(semester.semesterId)
.map { gradesSummary ->
gradesSummary.map {
GradeSummary(
semesterId = semester.semesterId,
studentId = semester.studentId,
position = 0,
subject = it.name,
predictedGrade = it.predicted,
finalGrade = it.final,
pointsSum = it.pointsSum,
proposedPoints = it.proposedPoints,
finalPoints = it.finalPoints,
average = it.average
)
}
}
}
}

View File

@ -1,35 +0,0 @@
package io.github.wulkanowy.data.repositories.gradessummary
import com.github.pwittchen.reactivenetwork.library.rx2.ReactiveNetwork
import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.InternetObservingSettings
import io.github.wulkanowy.data.db.entities.GradeSummary
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.utils.uniqueSubtract
import io.reactivex.Single
import java.net.UnknownHostException
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class GradeSummaryRepository @Inject constructor(
private val settings: InternetObservingSettings,
private val local: GradeSummaryLocal,
private val remote: GradeSummaryRemote
) {
fun getGradesSummary(student: Student, semester: Semester, forceRefresh: Boolean = false): Single<List<GradeSummary>> {
return local.getGradesSummary(semester).filter { !forceRefresh }
.switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings)
.flatMap {
if (it) remote.getGradeSummary(student, semester)
else Single.error(UnknownHostException())
}.flatMap { new ->
local.getGradesSummary(semester).toSingle(emptyList())
.doOnSuccess { old ->
local.deleteGradesSummary(old.uniqueSubtract(new))
local.saveGradesSummary(new.uniqueSubtract(old))
}
}.flatMap { local.getGradesSummary(semester).toSingle(emptyList()) })
}
}

View File

@ -5,7 +5,7 @@ import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.Inter
import io.github.wulkanowy.data.db.entities.Homework
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.utils.friday
import io.github.wulkanowy.utils.sunday
import io.github.wulkanowy.utils.monday
import io.github.wulkanowy.utils.uniqueSubtract
import io.reactivex.Completable
@ -23,7 +23,7 @@ class HomeworkRepository @Inject constructor(
) {
fun getHomework(student: Student, semester: Semester, start: LocalDate, end: LocalDate, forceRefresh: Boolean = false): Single<List<Homework>> {
return Single.fromCallable { start.monday to end.friday }.flatMap { (monday, friday) ->
return Single.fromCallable { start.monday to end.sunday }.flatMap { (monday, friday) ->
local.getHomework(semester, monday, friday).filter { !forceRefresh }
.switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings)
.flatMap {

View File

@ -75,6 +75,6 @@ class MessageRemote @Inject constructor(private val sdk: Sdk) {
}
fun deleteMessage(student: Student, message: Message): Single<Boolean> {
return sdk.init(student).deleteMessages(listOf(Pair(message.realId, message.folderId)))
return sdk.init(student).deleteMessages(listOf(message.messageId to message.folderId))
}
}

View File

@ -3,6 +3,7 @@ package io.github.wulkanowy.data.repositories.preferences
import android.content.Context
import android.content.SharedPreferences
import io.github.wulkanowy.R
import io.github.wulkanowy.ui.modules.grade.GradeAverageMode
import javax.inject.Inject
import javax.inject.Singleton
@ -17,8 +18,8 @@ class PreferencesRepository @Inject constructor(
val isShowPresent: Boolean
get() = getBoolean(R.string.pref_key_attendance_present, R.bool.pref_default_attendance_present)
val gradeAverageMode: String
get() = getString(R.string.pref_key_grade_average_mode, R.string.pref_default_grade_average_mode)
val gradeAverageMode: GradeAverageMode
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)
@ -26,6 +27,9 @@ class PreferencesRepository @Inject constructor(
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)
val appThemeKey = context.getString(R.string.pref_key_app_theme)
val appTheme: String
get() = getString(appThemeKey, R.string.pref_default_app_theme)
@ -52,6 +56,10 @@ class PreferencesRepository @Inject constructor(
val isNotificationsEnable: Boolean
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 isUpcomingLessonsNotificationsEnable: Boolean
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)
@ -68,6 +76,9 @@ class PreferencesRepository @Inject constructor(
val showWholeClassPlan: String
get() = getString(R.string.pref_key_timetable_show_whole_class, R.string.pref_default_timetable_show_whole_class)
val showTimetableTimers: Boolean
get() = getBoolean(R.string.pref_key_timetable_show_timers, R.bool.pref_default_timetable_show_timers)
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)

View File

@ -39,7 +39,7 @@ class StudentRemote @Inject constructor(private val sdk: Sdk) {
}
fun getStudentsMobileApi(token: String, pin: String, symbol: String): Single<List<Student>> {
return sdk.getStudentsFromMobileApi(token, pin, symbol).map { mapStudents(it, "", "") }
return sdk.getStudentsFromMobileApi(token, pin, symbol, "").map { mapStudents(it, "", "") }
}
fun getStudentsScrapper(email: String, password: String, scrapperBaseUrl: String, symbol: String): Single<List<Student>> {
@ -47,6 +47,6 @@ class StudentRemote @Inject constructor(private val sdk: Sdk) {
}
fun getStudentsHybrid(email: String, password: String, scrapperBaseUrl: String, symbol: String): Single<List<Student>> {
return sdk.getStudentsHybrid(email, password, scrapperBaseUrl, symbol).map { mapStudents(it, email, password) }
return sdk.getStudentsHybrid(email, password, scrapperBaseUrl, "", symbol).map { mapStudents(it, email, password) }
}
}

View File

@ -5,7 +5,8 @@ import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.Inter
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.Timetable
import io.github.wulkanowy.utils.friday
import io.github.wulkanowy.services.alarm.TimetableNotificationSchedulerHelper
import io.github.wulkanowy.utils.sunday
import io.github.wulkanowy.utils.monday
import io.github.wulkanowy.utils.uniqueSubtract
import io.reactivex.Single
@ -18,21 +19,22 @@ import javax.inject.Singleton
class TimetableRepository @Inject constructor(
private val settings: InternetObservingSettings,
private val local: TimetableLocal,
private val remote: TimetableRemote
private val remote: TimetableRemote,
private val schedulerHelper: TimetableNotificationSchedulerHelper
) {
fun getTimetable(student: Student, semester: Semester, start: LocalDate, end: LocalDate, forceRefresh: Boolean = false): Single<List<Timetable>> {
return Single.fromCallable { start.monday to end.friday }.flatMap { (monday, friday) ->
local.getTimetable(semester, monday, friday).filter { !forceRefresh }
return Single.fromCallable { start.monday to end.sunday }.flatMap { (monday, sunday) ->
local.getTimetable(semester, monday, sunday).filter { !forceRefresh }
.switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings).flatMap {
if (it) remote.getTimetable(student, semester, monday, friday)
if (it) remote.getTimetable(student, semester, monday, sunday)
else Single.error(UnknownHostException())
}.flatMap { new ->
local.getTimetable(semester, monday, friday)
local.getTimetable(semester, monday, sunday)
.toSingle(emptyList())
.doOnSuccess { old ->
local.deleteTimetable(old.uniqueSubtract(new))
local.saveTimetable(new.uniqueSubtract(old).map { item ->
local.deleteTimetable(old.uniqueSubtract(new).also { schedulerHelper.cancelScheduled(it) })
local.saveTimetable(new.uniqueSubtract(old).also { schedulerHelper.scheduleNotifications(it, student) }.map { item ->
item.also { new ->
old.singleOrNull { new.start == it.start }?.let { old ->
return@map new.copy(
@ -44,8 +46,8 @@ class TimetableRepository @Inject constructor(
})
}
}.flatMap {
local.getTimetable(semester, monday, friday).toSingle(emptyList())
}).map { list -> list.filter { it.date in start..end } }
local.getTimetable(semester, monday, sunday).toSingle(emptyList())
}).map { list -> list.filter { it.date in start..end }.also { schedulerHelper.scheduleNotifications(it, student) } }
}
}
}

View File

@ -5,8 +5,6 @@ import android.content.Context
import com.yariksoffice.lingver.Lingver
import dagger.Module
import dagger.Provides
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import io.github.wulkanowy.WulkanowyApp
import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.SchedulersProvider
@ -23,9 +21,6 @@ internal class AppModule {
@Provides
fun provideSchedulersProvider() = SchedulersProvider()
@Provides
fun provideFlexibleAdapter() = FlexibleAdapter<AbstractFlexibleItem<*>>(null, null, true)
@Singleton
@Provides
fun provideAppWidgetManager(context: Context): AppWidgetManager = AppWidgetManager.getInstance(context)

View File

@ -4,6 +4,7 @@ import dagger.Module
import dagger.android.ContributesAndroidInjector
import io.github.wulkanowy.di.scopes.PerActivity
import io.github.wulkanowy.ui.base.ErrorDialog
import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver
import io.github.wulkanowy.ui.modules.login.LoginActivity
import io.github.wulkanowy.ui.modules.login.LoginModule
import io.github.wulkanowy.ui.modules.luckynumberwidget.LuckyNumberWidgetConfigureActivity
@ -48,4 +49,7 @@ internal abstract class BindingModule {
@ContributesAndroidInjector
abstract fun bindLuckyNumberWidgetProvider(): LuckyNumberWidgetProvider
@ContributesAndroidInjector
abstract fun bindTimetableNotificationReceiver(): TimetableNotificationReceiver
}

View File

@ -1,7 +1,9 @@
package io.github.wulkanowy.services
import android.app.AlarmManager
import android.content.Context
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.getSystemService
import androidx.work.WorkManager
import com.squareup.inject.assisted.dagger2.AssistedModule
import dagger.Binds
@ -15,13 +17,13 @@ import io.github.wulkanowy.services.sync.channels.LuckyNumberChannel
import io.github.wulkanowy.services.sync.channels.NewGradesChannel
import io.github.wulkanowy.services.sync.channels.NewMessagesChannel
import io.github.wulkanowy.services.sync.channels.NewNotesChannel
import io.github.wulkanowy.services.sync.channels.UpcomingLessonsChannel
import io.github.wulkanowy.services.sync.channels.PushChannel
import io.github.wulkanowy.services.sync.works.AttendanceSummaryWork
import io.github.wulkanowy.services.sync.works.AttendanceWork
import io.github.wulkanowy.services.sync.works.CompletedLessonWork
import io.github.wulkanowy.services.sync.works.ExamWork
import io.github.wulkanowy.services.sync.works.GradeStatisticsWork
import io.github.wulkanowy.services.sync.works.GradeSummaryWork
import io.github.wulkanowy.services.sync.works.GradeWork
import io.github.wulkanowy.services.sync.works.HomeworkWork
import io.github.wulkanowy.services.sync.works.LuckyNumberWork
@ -47,6 +49,10 @@ abstract class ServicesModule {
@Singleton
@Provides
fun provideNotificationManager(context: Context) = NotificationManagerCompat.from(context)
@Singleton
@Provides
fun provideAlarmManager(context: Context): AlarmManager = context.getSystemService()!!
}
@ContributesAndroidInjector
@ -64,10 +70,6 @@ abstract class ServicesModule {
@IntoSet
abstract fun provideAttendanceWork(work: AttendanceWork): Work
@Binds
@IntoSet
abstract fun provideGradeSummaryWork(work: GradeSummaryWork): Work
@Binds
@IntoSet
abstract fun provideExamWork(work: ExamWork): Work
@ -131,4 +133,8 @@ abstract class ServicesModule {
@Binds
@IntoSet
abstract fun providePushChannel(channel: PushChannel): Channel
@Binds
@IntoSet
abstract fun provideUpcomingLessonsChannel(channel: UpcomingLessonsChannel): Channel
}

View File

@ -0,0 +1,117 @@
package io.github.wulkanowy.services.alarm
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Build.VERSION_CODES.N
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import dagger.android.AndroidInjection
import io.github.wulkanowy.R
import io.github.wulkanowy.data.repositories.student.StudentRepository
import io.github.wulkanowy.services.sync.channels.UpcomingLessonsChannel.Companion.CHANNEL_ID
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.SchedulersProvider
import io.github.wulkanowy.utils.getCompatColor
import io.github.wulkanowy.utils.toLocalDateTime
import timber.log.Timber
import javax.inject.Inject
class TimetableNotificationReceiver : BroadcastReceiver() {
@Inject
lateinit var studentRepository: StudentRepository
@Inject
lateinit var schedulers: SchedulersProvider
companion object {
const val NOTIFICATION_TYPE_CURRENT = 1
const val NOTIFICATION_TYPE_UPCOMING = 2
const val NOTIFICATION_TYPE_LAST_LESSON_CANCELLATION = 3
const val NOTIFICATION_ID = "id"
const val STUDENT_NAME = "student_name"
const val STUDENT_ID = "student_id"
const val LESSON_TYPE = "type"
const val LESSON_TITLE = "title"
const val LESSON_ROOM = "room"
const val LESSON_NEXT_TITLE = "next_title"
const val LESSON_NEXT_ROOM = "next_room"
const val LESSON_START = "start_timestamp"
const val LESSON_END = "end_timestamp"
}
@SuppressLint("CheckResult")
override fun onReceive(context: Context, intent: Intent) {
Timber.d("Receiving intent... ${intent.toUri(0)}")
AndroidInjection.inject(this, context)
studentRepository.getCurrentStudent(false)
.subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread)
.subscribe({
val studentId = intent.getIntExtra(STUDENT_ID, 0)
if (it.studentId == studentId) prepareNotification(context, intent)
else Timber.d("Notification studentId($studentId) differs from current(${it.studentId})")
}, { Timber.e(it) })
}
private fun prepareNotification(context: Context, intent: Intent) {
val type = intent.getIntExtra(LESSON_TYPE, 0)
val notificationId = intent.getIntExtra(NOTIFICATION_ID, MainView.Section.TIMETABLE.id)
if (type == NOTIFICATION_TYPE_LAST_LESSON_CANCELLATION) {
return NotificationManagerCompat.from(context).cancel(notificationId)
}
val studentId = intent.getIntExtra(STUDENT_ID, 0)
val studentName = intent.getStringExtra(STUDENT_NAME)
val subject = intent.getStringExtra(LESSON_TITLE)
val room = intent.getStringExtra(LESSON_ROOM)
val start = intent.getLongExtra(LESSON_START, 0)
val end = intent.getLongExtra(LESSON_END, 0)
val nextSubject = intent.getStringExtra(LESSON_NEXT_TITLE)
val nextRoom = intent.getStringExtra(LESSON_NEXT_ROOM)
Timber.d("TimetableNotification receive: type: $type, subject: $subject, start: ${start.toLocalDateTime()}, student: $studentId")
showNotification(context, notificationId, studentName,
if (type == NOTIFICATION_TYPE_CURRENT) end else start, end - start,
context.getString(if (type == NOTIFICATION_TYPE_CURRENT) R.string.timetable_now else R.string.timetable_next, "($room) $subject".removePrefix("()")),
nextSubject?.let { context.getString(R.string.timetable_later, "($nextRoom) $nextSubject".removePrefix("()")) }
)
}
private fun showNotification(context: Context, notificationId: Int, studentName: String?, countDown: Long, timeout: Long, title: String, next: String?) {
NotificationManagerCompat.from(context).notify(notificationId, NotificationCompat.Builder(context, CHANNEL_ID)
.setContentTitle(title)
.setContentText(next)
.setAutoCancel(false)
.setOngoing(true)
.setWhen(countDown)
.apply {
if (Build.VERSION.SDK_INT >= N) setUsesChronometer(true)
}
.setTimeoutAfter(timeout)
.setSmallIcon(R.drawable.ic_stat_timetable)
.setColor(context.getCompatColor(R.color.colorPrimary))
.setStyle(NotificationCompat.InboxStyle().also {
it.setSummaryText(studentName)
it.addLine(next)
})
.setContentIntent(PendingIntent.getActivity(context, MainView.Section.TIMETABLE.id,
MainActivity.getStartIntent(context, MainView.Section.TIMETABLE, true), FLAG_UPDATE_CURRENT))
.build()
)
}
}

View File

@ -0,0 +1,109 @@
package io.github.wulkanowy.services.alarm
import android.app.AlarmManager
import android.app.AlarmManager.RTC_WAKEUP
import android.app.PendingIntent
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import android.content.Context
import android.content.Intent
import androidx.core.app.AlarmManagerCompat
import androidx.core.app.NotificationManagerCompat
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.Timetable
import io.github.wulkanowy.data.repositories.preferences.PreferencesRepository
import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companion.LESSON_END
import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companion.LESSON_NEXT_ROOM
import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companion.LESSON_NEXT_TITLE
import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companion.LESSON_ROOM
import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companion.LESSON_START
import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companion.LESSON_TITLE
import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companion.LESSON_TYPE
import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companion.NOTIFICATION_ID
import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companion.NOTIFICATION_TYPE_CURRENT
import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companion.NOTIFICATION_TYPE_LAST_LESSON_CANCELLATION
import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companion.NOTIFICATION_TYPE_UPCOMING
import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companion.STUDENT_ID
import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companion.STUDENT_NAME
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.toTimestamp
import org.threeten.bp.LocalDateTime
import org.threeten.bp.LocalDateTime.now
import timber.log.Timber
import javax.inject.Inject
class TimetableNotificationSchedulerHelper @Inject constructor(
private val context: Context,
private val alarmManager: AlarmManager,
private val preferencesRepository: PreferencesRepository
) {
private fun getRequestCode(time: LocalDateTime, studentId: Int) = (time.toTimestamp() * studentId).toInt()
private fun getUpcomingLessonTime(index: Int, day: List<Timetable>, lesson: Timetable): LocalDateTime {
return day.getOrNull(index - 1)?.end ?: lesson.start.minusMinutes(30)
}
fun cancelScheduled(lessons: List<Timetable>, studentId: Int = 1) {
lessons.sortedBy { it.start }.forEachIndexed { index, lesson ->
val upcomingTime = getUpcomingLessonTime(index, lessons, lesson)
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")
}
}
private fun cancelScheduledTo(range: ClosedRange<LocalDateTime>, requestCode: Int) {
if (now() in range) cancelNotification()
alarmManager.cancel(PendingIntent.getBroadcast(context, requestCode, Intent(), FLAG_UPDATE_CURRENT))
}
fun cancelNotification() = NotificationManagerCompat.from(context).cancel(MainView.Section.TIMETABLE.id)
fun scheduleNotifications(lessons: List<Timetable>, student: Student) {
if (!preferencesRepository.isUpcomingLessonsNotificationsEnable) return cancelScheduled(lessons, student.studentId)
lessons.groupBy { it.date }
.map { it.value.sortedBy { lesson -> lesson.start } }
.map { it.filter { lesson -> !lesson.canceled && lesson.isStudentPlan } }
.map { day ->
day.forEachIndexed { index, lesson ->
val intent = createIntent(student, lesson, day.getOrNull(index + 1))
if (lesson.start > now()) {
scheduleBroadcast(intent, student.studentId, NOTIFICATION_TYPE_UPCOMING, getUpcomingLessonTime(index, day, lesson))
}
if (lesson.end > now()) {
scheduleBroadcast(intent, student.studentId, NOTIFICATION_TYPE_CURRENT, lesson.start)
if (day.lastIndex == index) {
scheduleBroadcast(intent, student.studentId, NOTIFICATION_TYPE_LAST_LESSON_CANCELLATION, lesson.end)
}
}
}
}
}
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.studentName)
putExtra(LESSON_ROOM, lesson.room)
putExtra(LESSON_START, lesson.start.toTimestamp())
putExtra(LESSON_END, lesson.end.toTimestamp())
putExtra(LESSON_TITLE, lesson.subject)
putExtra(LESSON_NEXT_TITLE, nextLesson?.subject)
putExtra(LESSON_NEXT_ROOM, nextLesson?.room)
}
}
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")
}
}

View File

@ -42,7 +42,7 @@ class SyncManager @Inject constructor(
init {
if (now().isHolidays) stopSyncWorker()
if (SDK_INT > O) {
if (SDK_INT >= O) {
channels.forEach { it.create() }
notificationManager.deleteNotificationChannel("new_entries_channel")
}

View File

@ -22,7 +22,7 @@ class DebugChannel @Inject constructor(
}
override fun create() {
if (appInfo.isDebug) return
if (!appInfo.isDebug) return
notificationManager.createNotificationChannel(
NotificationChannel(CHANNEL_ID, context.getString(R.string.channel_debug), IMPORTANCE_DEFAULT)
.apply {

View File

@ -0,0 +1,31 @@
package io.github.wulkanowy.services.sync.channels
import android.annotation.TargetApi
import android.app.Notification.VISIBILITY_PUBLIC
import android.app.NotificationChannel
import android.app.NotificationManager.IMPORTANCE_DEFAULT
import android.content.Context
import androidx.core.app.NotificationManagerCompat
import io.github.wulkanowy.R
import javax.inject.Inject
@TargetApi(26)
class UpcomingLessonsChannel @Inject constructor(
private val notificationManager: NotificationManagerCompat,
private val context: Context
) : Channel {
companion object {
const val CHANNEL_ID = "lesson_channel"
}
override fun create() {
notificationManager.createNotificationChannel(
NotificationChannel(CHANNEL_ID, context.getString(R.string.channel_upcoming_lessons), IMPORTANCE_DEFAULT).apply {
lockscreenVisibility = VISIBILITY_PUBLIC
setShowBadge(false)
enableVibration(false)
}
)
}
}

View File

@ -3,7 +3,7 @@ package io.github.wulkanowy.services.sync.works
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.repositories.attendance.AttendanceRepository
import io.github.wulkanowy.utils.friday
import io.github.wulkanowy.utils.sunday
import io.github.wulkanowy.utils.monday
import io.reactivex.Completable
import org.threeten.bp.LocalDate.now
@ -12,7 +12,7 @@ import javax.inject.Inject
class AttendanceWork @Inject constructor(private val attendanceRepository: AttendanceRepository) : Work {
override fun create(student: Student, semester: Semester): Completable {
return attendanceRepository.getAttendance(student, semester, now().monday, now().friday, true)
return attendanceRepository.getAttendance(student, semester, now().monday, now().sunday, true)
.ignoreElement()
}
}

View File

@ -3,7 +3,7 @@ package io.github.wulkanowy.services.sync.works
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.repositories.completedlessons.CompletedLessonsRepository
import io.github.wulkanowy.utils.friday
import io.github.wulkanowy.utils.sunday
import io.github.wulkanowy.utils.monday
import io.reactivex.Completable
import org.threeten.bp.LocalDate.now
@ -14,7 +14,7 @@ class CompletedLessonWork @Inject constructor(
) : Work {
override fun create(student: Student, semester: Semester): Completable {
return completedLessonsRepository.getCompletedLessons(student, semester, now().monday, now().friday, true)
return completedLessonsRepository.getCompletedLessons(student, semester, now().monday, now().sunday, true)
.ignoreElement()
}
}

View File

@ -3,7 +3,7 @@ package io.github.wulkanowy.services.sync.works
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.repositories.exam.ExamRepository
import io.github.wulkanowy.utils.friday
import io.github.wulkanowy.utils.sunday
import io.github.wulkanowy.utils.monday
import io.reactivex.Completable
import org.threeten.bp.LocalDate.now
@ -12,6 +12,6 @@ import javax.inject.Inject
class ExamWork @Inject constructor(private val examRepository: ExamRepository) : Work {
override fun create(student: Student, semester: Semester): Completable {
return examRepository.getExams(student, semester, now().monday, now().friday, true).ignoreElement()
return examRepository.getExams(student, semester, now().monday, now().sunday, true).ignoreElement()
}
}

View File

@ -1,14 +0,0 @@
package io.github.wulkanowy.services.sync.works
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.repositories.gradessummary.GradeSummaryRepository
import io.reactivex.Completable
import javax.inject.Inject
class GradeSummaryWork @Inject constructor(private val gradeSummaryRepository: GradeSummaryRepository) : Work {
override fun create(student: Student, semester: Semester): Completable {
return gradeSummaryRepository.getGradesSummary(student, semester, true).ignoreElement()
}
}

View File

@ -58,4 +58,3 @@ class GradeWork @Inject constructor(
)
}
}

View File

@ -3,7 +3,7 @@ package io.github.wulkanowy.services.sync.works
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.repositories.homework.HomeworkRepository
import io.github.wulkanowy.utils.friday
import io.github.wulkanowy.utils.sunday
import io.github.wulkanowy.utils.monday
import io.reactivex.Completable
import org.threeten.bp.LocalDate.now
@ -12,6 +12,6 @@ import javax.inject.Inject
class HomeworkWork @Inject constructor(private val homeworkRepository: HomeworkRepository) : Work {
override fun create(student: Student, semester: Semester): Completable {
return homeworkRepository.getHomework(student, semester, now().monday, now().friday, true).ignoreElement()
return homeworkRepository.getHomework(student, semester, now().monday, now().sunday, true).ignoreElement()
}
}

View File

@ -3,7 +3,7 @@ package io.github.wulkanowy.services.sync.works
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.repositories.timetable.TimetableRepository
import io.github.wulkanowy.utils.friday
import io.github.wulkanowy.utils.sunday
import io.github.wulkanowy.utils.monday
import io.reactivex.Completable
import org.threeten.bp.LocalDate.now
@ -12,7 +12,7 @@ import javax.inject.Inject
class TimetableWork @Inject constructor(private val timetableRepository: TimetableRepository) : Work {
override fun create(student: Student, semester: Semester): Completable {
return timetableRepository.getTimetable(student, semester, now().monday, now().friday, true)
return timetableRepository.getTimetable(student, semester, now().monday, now().sunday, true)
.ignoreElement()
}
}

View File

@ -11,6 +11,7 @@ import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.viewbinding.ViewBinding
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.snackbar.Snackbar.LENGTH_LONG
import dagger.android.AndroidInjection
@ -20,10 +21,13 @@ import io.github.wulkanowy.R
import io.github.wulkanowy.ui.modules.login.LoginActivity
import io.github.wulkanowy.utils.FragmentLifecycleLogger
import io.github.wulkanowy.utils.getThemeAttrColor
import io.github.wulkanowy.utils.lifecycleAwareVariable
import javax.inject.Inject
abstract class BaseActivity<T : BasePresenter<out BaseView>> : AppCompatActivity(), BaseView,
HasAndroidInjector {
abstract class BaseActivity<T : BasePresenter<out BaseView>, VB : ViewBinding> :
AppCompatActivity(), BaseView, HasAndroidInjector {
protected var binding: VB by lifecycleAwareVariable()
@Inject
lateinit var androidInjector: DispatchingAndroidInjector<Any>

View File

@ -1,9 +1,13 @@
package io.github.wulkanowy.ui.base
import android.widget.Toast
import androidx.viewbinding.ViewBinding
import dagger.android.support.DaggerAppCompatDialogFragment
import io.github.wulkanowy.utils.lifecycleAwareVariable
abstract class BaseDialogFragment : DaggerAppCompatDialogFragment(), BaseView {
abstract class BaseDialogFragment<VB : ViewBinding> : DaggerAppCompatDialogFragment(), BaseView {
protected var binding: VB by lifecycleAwareVariable()
override fun showError(text: String, error: Throwable) {
showMessage(text)
@ -14,11 +18,11 @@ abstract class BaseDialogFragment : DaggerAppCompatDialogFragment(), BaseView {
}
override fun showExpiredDialog() {
(activity as? BaseActivity<*>)?.showExpiredDialog()
(activity as? BaseActivity<*, *>)?.showExpiredDialog()
}
override fun openClearLoginView() {
(activity as? BaseActivity<*>)?.openClearLoginView()
(activity as? BaseActivity<*, *>)?.openClearLoginView()
}
override fun showErrorDetailsDialog(error: Throwable) {

View File

@ -0,0 +1,58 @@
package io.github.wulkanowy.ui.base
import android.util.DisplayMetrics
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.LinearSmoothScroller
import androidx.recyclerview.widget.RecyclerView
import kotlin.math.max
import kotlin.math.min
abstract class BaseExpandableAdapter<T : RecyclerView.ViewHolder> : RecyclerView.Adapter<T>() {
companion object {
private const val MILLISECONDS_PER_INCH = 100f
private const val AUTO_SCROLL_DELAY = 150L
}
private var recyclerView: RecyclerView? = null
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
super.onAttachedToRecyclerView(recyclerView)
this.recyclerView = recyclerView
}
override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
super.onDetachedFromRecyclerView(recyclerView)
this.recyclerView = null
}
// original: https://github.com/davideas/FlexibleAdapter/blob/5.1.0/flexible-adapter/src/main/java/eu/davidea/flexibleadapter/FlexibleAdapter.java#L4984-L5011
protected fun scrollToHeaderWithSubItems(position: Int, subItemsCount: Int) {
val layoutManager = recyclerView!!.layoutManager as LinearLayoutManager
val firstVisibleItem = layoutManager.findFirstCompletelyVisibleItemPosition()
val lastVisibleItem = layoutManager.findLastCompletelyVisibleItemPosition()
val itemsToShow = position + subItemsCount - lastVisibleItem
if (itemsToShow > 0) {
val scrollMax: Int = position - firstVisibleItem
val scrollMin = max(0, position + subItemsCount - lastVisibleItem)
val scrollBy = min(scrollMax, scrollMin)
val scrollTo = firstVisibleItem + scrollBy
scrollToPosition(scrollTo)
} else if (position < firstVisibleItem) {
scrollToPosition(position)
}
}
private fun scrollToPosition(position: Int) {
recyclerView?.run {
postDelayed({
layoutManager?.startSmoothScroll(object : LinearSmoothScroller(context) {
override fun getVerticalSnapPreference() = SNAP_TO_START
override fun calculateSpeedPerPixel(displayMetrics: DisplayMetrics) = MILLISECONDS_PER_INCH / displayMetrics.densityDpi
}.apply {
targetPosition = position
})
}, AUTO_SCROLL_DELAY)
}
}
}

View File

@ -1,12 +1,18 @@
package io.github.wulkanowy.ui.base
import android.view.View
import androidx.annotation.LayoutRes
import androidx.viewbinding.ViewBinding
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.snackbar.Snackbar.LENGTH_LONG
import dagger.android.support.DaggerFragment
import io.github.wulkanowy.R
import io.github.wulkanowy.utils.lifecycleAwareVariable
abstract class BaseFragment : DaggerFragment(), BaseView {
abstract class BaseFragment<VB : ViewBinding>(@LayoutRes layoutId: Int) : DaggerFragment(layoutId),
BaseView {
protected var binding: VB by lifecycleAwareVariable()
protected var messageContainer: View? = null
@ -16,7 +22,7 @@ abstract class BaseFragment : DaggerFragment(), BaseView {
.setAction(R.string.all_details) { if (isAdded) showErrorDetailsDialog(error) }
.show()
} else {
(activity as? BaseActivity<*>)?.showError(text, error)
(activity as? BaseActivity<*, *>)?.showError(text, error)
}
}
@ -28,15 +34,15 @@ abstract class BaseFragment : DaggerFragment(), BaseView {
if (messageContainer != null) {
Snackbar.make(messageContainer!!, text, LENGTH_LONG).show()
} else {
(activity as? BaseActivity<*>)?.showMessage(text)
(activity as? BaseActivity<*, *>)?.showMessage(text)
}
}
override fun showExpiredDialog() {
(activity as? BaseActivity<*>)?.showExpiredDialog()
(activity as? BaseActivity<*, *>)?.showExpiredDialog()
}
override fun openClearLoginView() {
(activity as? BaseActivity<*>)?.openClearLoginView()
(activity as? BaseActivity<*, *>)?.openClearLoginView()
}
}

View File

@ -9,16 +9,18 @@ import android.view.ViewGroup
import android.widget.HorizontalScrollView
import android.widget.Toast
import android.widget.Toast.LENGTH_LONG
import androidx.appcompat.app.AlertDialog
import androidx.core.content.getSystemService
import io.github.wulkanowy.R
import io.github.wulkanowy.databinding.DialogErrorBinding
import io.github.wulkanowy.sdk.exception.FeatureDisabledException
import io.github.wulkanowy.sdk.exception.FeatureNotAvailableException
import io.github.wulkanowy.sdk.exception.ServiceUnavailableException
import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.getString
import io.github.wulkanowy.utils.openAppInMarket
import io.github.wulkanowy.utils.openEmailClient
import io.github.wulkanowy.utils.openInternetBrowser
import kotlinx.android.synthetic.main.dialog_error.*
import java.io.InterruptedIOException
import java.io.PrintWriter
import java.io.StringWriter
@ -26,7 +28,7 @@ import java.net.SocketTimeoutException
import java.net.UnknownHostException
import javax.inject.Inject
class ErrorDialog : BaseDialogFragment() {
class ErrorDialog : BaseDialogFragment<DialogErrorBinding>() {
private lateinit var error: Throwable
@ -52,7 +54,7 @@ class ErrorDialog : BaseDialogFragment() {
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.dialog_error, container, false)
return DialogErrorBinding.inflate(inflater).apply { binding = this }.root
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
@ -62,30 +64,45 @@ class ErrorDialog : BaseDialogFragment() {
error.printStackTrace(PrintWriter(this))
}
errorDialogContent.text = stringWriter.toString()
with(errorDialogHorizontalScroll) {
post { fullScroll(HorizontalScrollView.FOCUS_LEFT) }
}
errorDialogCopy.setOnClickListener {
val clip = ClipData.newPlainText("wulkanowy", stringWriter.toString())
activity?.getSystemService<ClipboardManager>()?.setPrimaryClip(clip)
with(binding) {
errorDialogContent.text = stringWriter.toString()
with(errorDialogHorizontalScroll) {
post { fullScroll(HorizontalScrollView.FOCUS_LEFT) }
}
errorDialogCopy.setOnClickListener {
val clip = ClipData.newPlainText("wulkanowy", stringWriter.toString())
activity?.getSystemService<ClipboardManager>()?.setPrimaryClip(clip)
Toast.makeText(context, R.string.all_copied, LENGTH_LONG).show()
}
errorDialogCancel.setOnClickListener { dismiss() }
errorDialogReport.setOnClickListener { openEmailClient(stringWriter.toString()) }
errorDialogMessage.text = resources.getString(error)
errorDialogReport.isEnabled = when (error) {
is UnknownHostException,
is InterruptedIOException,
is SocketTimeoutException,
is ServiceUnavailableException,
is FeatureDisabledException,
is FeatureNotAvailableException -> false
else -> true
Toast.makeText(context, R.string.all_copied, LENGTH_LONG).show()
}
errorDialogCancel.setOnClickListener { dismiss() }
errorDialogReport.setOnClickListener {
openConfirmDialog { openEmailClient(stringWriter.toString()) }
}
errorDialogMessage.text = resources.getString(error)
errorDialogReport.isEnabled = when (error) {
is UnknownHostException,
is InterruptedIOException,
is SocketTimeoutException,
is ServiceUnavailableException,
is FeatureDisabledException,
is FeatureNotAvailableException -> false
else -> true
}
}
}
private fun openConfirmDialog(callback: () -> Unit) {
AlertDialog.Builder(requireContext())
.setTitle(R.string.dialog_error_check_update)
.setMessage(R.string.dialog_error_check_update_message)
.setNeutralButton(R.string.about_feedback) { _, _ -> callback() }
.setPositiveButton(R.string.dialog_error_check_update) { _, _ ->
requireContext().openAppInMarket(::showMessage)
}
.show()
}
private fun openEmailClient(content: String) {
requireContext().openEmailClient(
chooserTitle = getString(R.string.about_feedback),

View File

@ -0,0 +1,46 @@
package io.github.wulkanowy.ui.base
import android.annotation.SuppressLint
import android.graphics.PorterDuff
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
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 javax.inject.Inject
class WidgetConfigureAdapter @Inject constructor() : RecyclerView.Adapter<WidgetConfigureAdapter.ItemViewHolder>() {
var items = emptyList<Pair<Student, Boolean>>()
var onClickListener: (Student) -> Unit = {}
override fun getItemCount() = items.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ItemViewHolder(
ItemAccountBinding.inflate(LayoutInflater.from(parent.context), parent, false)
)
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
val (student, isCurrent) = items[position]
with(holder.binding) {
accountItemName.text = "${student.studentName} ${student.className}"
accountItemSchool.text = student.schoolName
with(accountItemImage) {
val colorImage = if (isCurrent) context.getThemeAttrColor(R.attr.colorPrimary)
else context.getThemeAttrColor(R.attr.colorOnSurface, 153)
setColorFilter(colorImage, PorterDuff.Mode.SRC_IN)
}
root.setOnClickListener { onClickListener(student) }
}
}
class ItemViewHolder(val binding: ItemAccountBinding) : RecyclerView.ViewHolder(binding.root)
}

View File

@ -0,0 +1,72 @@
package io.github.wulkanowy.ui.modules.about
import android.graphics.drawable.Drawable
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.content.res.ResourcesCompat
import androidx.recyclerview.widget.RecyclerView
import io.github.wulkanowy.databinding.ItemAboutBinding
import io.github.wulkanowy.databinding.ScrollableHeaderAboutBinding
import javax.inject.Inject
class AboutAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private enum class ViewType(val id: Int) {
ITEM_HEADER(1),
ITEM_ELEMENT(2)
}
var items = emptyList<Triple<String, String, Drawable?>>()
var onClickListener: (name: String) -> Unit = {}
override fun getItemCount() = items.size + 1
override fun getItemViewType(position: Int) = when (position) {
0 -> ViewType.ITEM_HEADER.id
else -> ViewType.ITEM_ELEMENT.id
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context)
return when (viewType) {
ViewType.ITEM_HEADER.id -> HeaderViewHolder(ScrollableHeaderAboutBinding.inflate(inflater, parent, false))
ViewType.ITEM_ELEMENT.id -> ItemViewHolder(ItemAboutBinding.inflate(inflater, parent, false))
else -> throw IllegalStateException()
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is HeaderViewHolder -> bindHeaderViewHolder(holder.binding)
is ItemViewHolder -> bindItemViewHolder(holder.binding, position - 1)
}
}
private fun bindHeaderViewHolder(binding: ScrollableHeaderAboutBinding) {
with(binding.aboutScrollableHeaderIcon) {
setImageDrawable(ResourcesCompat.getDrawableForDensity(
context.resources, context.applicationInfo.icon, 640, null)
)
}
}
private fun bindItemViewHolder(binding: ItemAboutBinding, position: Int) {
val (title, summary, image) = items[position]
with(binding) {
aboutItemImage.setImageDrawable(image)
aboutItemTitle.text = title
aboutItemSummary.text = summary
root.setOnClickListener { onClickListener(title) }
}
}
private class HeaderViewHolder(val binding: ScrollableHeaderAboutBinding) :
RecyclerView.ViewHolder(binding.root)
private class ItemViewHolder(val binding: ItemAboutBinding) :
RecyclerView.ViewHolder(binding.root)
}

View File

@ -2,13 +2,10 @@ package io.github.wulkanowy.ui.modules.about
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import androidx.recyclerview.widget.LinearLayoutManager
import io.github.wulkanowy.R
import io.github.wulkanowy.databinding.FragmentAboutBinding
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.about.contributor.ContributorFragment
import io.github.wulkanowy.ui.modules.about.license.LicenseFragment
@ -17,19 +14,19 @@ import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.AppInfo
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.setOnItemClickListener
import kotlinx.android.synthetic.main.fragment_about.*
import javax.inject.Inject
class AboutFragment : BaseFragment(), AboutView, MainView.TitledView {
class AboutFragment : BaseFragment<FragmentAboutBinding>(R.layout.fragment_about), AboutView,
MainView.TitledView {
@Inject
lateinit var presenter: AboutPresenter
@Inject
lateinit var aboutAdapter: FlexibleAdapter<AbstractFlexibleItem<*>>
lateinit var aboutAdapter: AboutAdapter
@Inject
lateinit var appInfo: AppInfo
@ -80,34 +77,34 @@ class AboutFragment : BaseFragment(), AboutView, MainView.TitledView {
fun newInstance() = AboutFragment()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_about, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentAboutBinding.bind(view)
presenter.onAttachView(this)
}
override fun initView() {
aboutAdapter.setOnItemClickListener(presenter::onItemSelected)
aboutAdapter.onClickListener = presenter::onItemSelected
with(aboutRecycler) {
layoutManager = SmoothScrollLinearLayoutManager(context)
with(binding.aboutRecycler) {
layoutManager = LinearLayoutManager(context)
adapter = aboutAdapter
}
}
override fun updateData(header: AboutScrollableHeader, items: List<AboutItem>) {
override fun updateData(data: List<Triple<String, String, Drawable?>>) {
with(aboutAdapter) {
removeAllScrollableHeaders()
addScrollableHeader(header)
updateDataSet(items)
items = data
notifyDataSetChanged()
}
}
override fun openAppInMarket() {
context?.openAppInMarket(::showMessage)
}
override fun openLogViewer() {
if (appInfo.isDebug) (activity as? MainActivity)?.pushView(LogViewerFragment.newInstance())
(activity as? MainActivity)?.pushView(LogViewerFragment.newInstance())
}
override fun openDiscordInvite() {
@ -123,7 +120,7 @@ class AboutFragment : BaseFragment(), AboutView, MainView.TitledView {
chooserTitle = getString(R.string.about_feedback),
email = "wulkanowyinc@gmail.com",
subject = "Zgłoszenie błędu",
body = requireContext().getString(R.string.about_feedback_template,
body = getString(R.string.about_feedback_template,
"${appInfo.systemManufacturer} ${appInfo.systemModel}", appInfo.systemVersion.toString(), appInfo.versionName
),
onActivityNotFound = {

View File

@ -1,56 +0,0 @@
package io.github.wulkanowy.ui.modules.about
import android.graphics.drawable.Drawable
import android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import eu.davidea.flexibleadapter.items.IFlexible
import eu.davidea.viewholders.FlexibleViewHolder
import io.github.wulkanowy.R
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.item_about.*
class AboutItem(
val title: String,
private val summary: String,
private val image: Drawable?
) : AbstractFlexibleItem<AboutItem.ViewHolder>() {
override fun getLayoutRes() = R.layout.item_about
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<*>>) = ViewHolder(view, adapter)
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<*>>, holder: ViewHolder, position: Int, payloads: MutableList<Any>) {
with(holder) {
aboutItemImage.setImageDrawable(image)
aboutItemTitle.text = title
aboutItemSummary.text = summary
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as AboutItem
if (title != other.title) return false
if (summary != other.summary) return false
if (image != other.image) return false
return true
}
override fun hashCode(): Int {
var result = title.hashCode()
result = 31 * result + summary.hashCode()
result = 31 * result + (image?.hashCode() ?: 0)
return result
}
class ViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<*>>) : FlexibleViewHolder(view, adapter),
LayoutContainer {
override val containerView: View? get() = contentView
}
}

View File

@ -1,9 +1,9 @@
package io.github.wulkanowy.ui.modules.about
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import io.github.wulkanowy.data.repositories.student.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.FirebaseAnalyticsHelper
import io.github.wulkanowy.utils.SchedulersProvider
import timber.log.Timber
@ -13,6 +13,7 @@ class AboutPresenter @Inject constructor(
schedulers: SchedulersProvider,
errorHandler: ErrorHandler,
studentRepository: StudentRepository,
private val appInfo: AppInfo,
private val analytics: FirebaseAnalyticsHelper
) : BasePresenter<AboutView>(errorHandler, studentRepository, schedulers) {
@ -23,13 +24,13 @@ class AboutPresenter @Inject constructor(
loadData()
}
fun onItemSelected(item: AbstractFlexibleItem<*>) {
if (item !is AboutItem) return
fun onItemSelected(name: String) {
view?.run {
when (item.title) {
when (name) {
versionRes?.first -> {
Timber.i("Opening log viewer")
openLogViewer()
if (appInfo.isDebug) openLogViewer()
else openAppInMarket()
analytics.logEvent("about_open", "name" to "log_viewer")
}
feedbackRes?.first -> {
@ -73,15 +74,16 @@ class AboutPresenter @Inject constructor(
private fun loadData() {
view?.run {
updateData(AboutScrollableHeader(), listOfNotNull(
versionRes?.let { (title, summary, image) -> AboutItem(title, summary, image) },
creatorsRes?.let { (title, summary, image) -> AboutItem(title, summary, image) },
feedbackRes?.let { (title, summary, image) -> AboutItem(title, summary, image) },
faqRes?.let { (title, summary, image) -> AboutItem(title, summary, image) },
discordRes?.let { (title, summary, image) -> AboutItem(title, summary, image) },
homepageRes?.let { (title, summary, image) -> AboutItem(title, summary, image) },
licensesRes?.let { (title, summary, image) -> AboutItem(title, summary, image) },
privacyRes?.let { (title, summary, image) -> AboutItem(title, summary, image) }))
updateData(listOfNotNull(
versionRes,
creatorsRes,
feedbackRes,
faqRes,
discordRes,
homepageRes,
licensesRes,
privacyRes
))
}
}
}

View File

@ -1,41 +0,0 @@
package io.github.wulkanowy.ui.modules.about
import android.view.View
import androidx.core.content.res.ResourcesCompat
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import eu.davidea.flexibleadapter.items.IFlexible
import eu.davidea.viewholders.FlexibleViewHolder
import io.github.wulkanowy.R
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.scrollable_header_about.*
class AboutScrollableHeader : AbstractFlexibleItem<AboutScrollableHeader.ViewHolder>() {
override fun getLayoutRes() = R.layout.scrollable_header_about
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<*>>) = ViewHolder(view, adapter)
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<*>>, holder: ViewHolder, position: Int, payloads: MutableList<Any>) {
with(holder) {
val context = contentView.context
val drawable = ResourcesCompat.getDrawableForDensity(context.resources, context.applicationInfo.icon, 640, null)
aboutScrollableHeaderIcon.setImageDrawable(drawable)
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
return true
}
override fun hashCode() = javaClass.hashCode()
class ViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<*>>) : FlexibleViewHolder(view, adapter),
LayoutContainer {
override val containerView: View? get() = contentView
}
}

View File

@ -23,7 +23,9 @@ interface AboutView : BaseView {
fun initView()
fun updateData(header: AboutScrollableHeader, items: List<AboutItem>)
fun updateData(data: List<Triple<String, String, Drawable?>>)
fun openAppInMarket()
fun openLogViewer()

View File

@ -0,0 +1,41 @@
package io.github.wulkanowy.ui.modules.about.contributor
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import coil.api.load
import coil.transform.RoundedCornersTransformation
import io.github.wulkanowy.data.pojos.Contributor
import io.github.wulkanowy.databinding.ItemContributorBinding
import javax.inject.Inject
class ContributorAdapter @Inject constructor() :
RecyclerView.Adapter<ContributorAdapter.ItemViewHolder>() {
var items = emptyList<Contributor>()
var onClickListener: (Contributor) -> Unit = {}
override fun getItemCount() = items.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ItemViewHolder(
ItemContributorBinding.inflate(LayoutInflater.from(parent.context), parent, false)
)
override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
val item = items[position]
with(holder.binding) {
creatorItemName.text = item.displayName
creatorItemAvatar.load("https://github.com/${item.githubUsername}.png") {
transformations(RoundedCornersTransformation(8f))
crossfade(true)
}
root.setOnClickListener { onClickListener(item) }
}
}
class ItemViewHolder(val binding: ItemContributorBinding) :
RecyclerView.ViewHolder(binding.root)
}

View File

@ -1,30 +1,27 @@
package io.github.wulkanowy.ui.modules.about.contributor
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import android.view.ViewGroup
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.common.FlexibleItemDecoration
import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import androidx.recyclerview.widget.LinearLayoutManager
import io.github.wulkanowy.R
import io.github.wulkanowy.data.pojos.Contributor
import io.github.wulkanowy.databinding.FragmentContributorBinding
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.openInternetBrowser
import io.github.wulkanowy.utils.setOnItemClickListener
import kotlinx.android.synthetic.main.fragment_creator.*
import javax.inject.Inject
class ContributorFragment : BaseFragment(), ContributorView, MainView.TitledView {
class ContributorFragment : BaseFragment<FragmentContributorBinding>(R.layout.fragment_contributor),
ContributorView, MainView.TitledView {
@Inject
lateinit var presenter: ContributorPresenter
@Inject
lateinit var creatorsAdapter: FlexibleAdapter<AbstractFlexibleItem<*>>
lateinit var creatorsAdapter: ContributorAdapter
override val titleStringId get() = R.string.contributors_title
@ -32,29 +29,27 @@ class ContributorFragment : BaseFragment(), ContributorView, MainView.TitledView
fun newInstance() = ContributorFragment()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_creator, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentContributorBinding.bind(view)
presenter.onAttachView(this)
}
override fun initView() {
with(creatorRecycler) {
layoutManager = SmoothScrollLinearLayoutManager(context)
with(binding.creatorRecycler) {
layoutManager = LinearLayoutManager(context)
adapter = creatorsAdapter
addItemDecoration(FlexibleItemDecoration(context)
.withDefaultDivider()
.withDrawDividerOnLastItem(false))
addItemDecoration(DividerItemDecoration(context))
}
creatorsAdapter.setOnItemClickListener(presenter::onItemSelected)
creatorSeeMore.setOnClickListener { presenter.onSeeMoreClick() }
creatorsAdapter.onClickListener = presenter::onItemSelected
binding.creatorSeeMore.setOnClickListener { presenter.onSeeMoreClick() }
}
override fun updateData(data: List<ContributorItem>) {
creatorsAdapter.updateDataSet(data)
override fun updateData(data: List<Contributor>) {
with(creatorsAdapter) {
items = data
notifyDataSetChanged()
}
}
override fun openUserGithubPage(username: String) {
@ -66,7 +61,7 @@ class ContributorFragment : BaseFragment(), ContributorView, MainView.TitledView
}
override fun showProgress(show: Boolean) {
creatorProgress.visibility = if (show) VISIBLE else GONE
binding.creatorProgress.visibility = if (show) VISIBLE else GONE
}
override fun onDestroyView() {

View File

@ -1,51 +0,0 @@
package io.github.wulkanowy.ui.modules.about.contributor
import android.view.View
import coil.api.load
import coil.transform.RoundedCornersTransformation
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import eu.davidea.flexibleadapter.items.IFlexible
import eu.davidea.viewholders.FlexibleViewHolder
import io.github.wulkanowy.R
import io.github.wulkanowy.data.pojos.AppCreator
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.item_contributor.*
class ContributorItem(val creator: AppCreator) :
AbstractFlexibleItem<ContributorItem.ViewHolder>() {
override fun getLayoutRes() = R.layout.item_contributor
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<*>>) = ViewHolder(view, adapter)
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<*>>, holder: ViewHolder, position: Int, payloads: MutableList<Any>) {
with(holder) {
creatorItemName.text = creator.displayName
creatorItemAvatar.load("https://github.com/${creator.githubUsername}.png") {
transformations(RoundedCornersTransformation(8f))
crossfade(true)
}
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as ContributorItem
if (creator != other.creator) return false
return true
}
override fun hashCode() = creator.hashCode()
class ViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<*>>) : FlexibleViewHolder(view, adapter),
LayoutContainer {
override val containerView: View? get() = contentView
}
}

View File

@ -1,6 +1,6 @@
package io.github.wulkanowy.ui.modules.about.contributor
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import io.github.wulkanowy.data.pojos.Contributor
import io.github.wulkanowy.data.repositories.appcreator.AppCreatorRepository
import io.github.wulkanowy.data.repositories.student.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter
@ -21,9 +21,8 @@ class ContributorPresenter @Inject constructor(
loadData()
}
fun onItemSelected(item: AbstractFlexibleItem<*>) {
if (item !is ContributorItem) return
view?.openUserGithubPage(item.creator.githubUsername)
fun onItemSelected(contributor: Contributor) {
view?.openUserGithubPage(contributor.githubUsername)
}
fun onSeeMoreClick() {
@ -32,7 +31,6 @@ class ContributorPresenter @Inject constructor(
private fun loadData() {
disposable.add(appCreatorRepository.getAppCreators()
.map { it.map { creator -> ContributorItem(creator) } }
.subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread)
.doFinally { view?.showProgress(false) }

View File

@ -1,12 +1,13 @@
package io.github.wulkanowy.ui.modules.about.contributor
import io.github.wulkanowy.data.pojos.Contributor
import io.github.wulkanowy.ui.base.BaseView
interface ContributorView : BaseView {
fun initView()
fun updateData(data: List<ContributorItem>)
fun updateData(data: List<Contributor>)
fun openUserGithubPage(username: String)

View File

@ -0,0 +1,34 @@
package io.github.wulkanowy.ui.modules.about.license
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.mikepenz.aboutlibraries.entity.Library
import io.github.wulkanowy.databinding.ItemLicenseBinding
import javax.inject.Inject
class LicenseAdapter @Inject constructor() : RecyclerView.Adapter<LicenseAdapter.ItemViewHolder>() {
var items = emptyList<Library>()
var onClickListener: (Library) -> Unit = {}
override fun getItemCount() = items.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ItemViewHolder(
ItemLicenseBinding.inflate(LayoutInflater.from(parent.context), parent, false)
)
override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
val item = items[position]
with(holder.binding) {
licenseItemName.text = item.libraryName
licenseItemSummary.text = item.license?.licenseName?.takeIf { it.isNotBlank() } ?: item.libraryVersion
root.setOnClickListener { onClickListener(item) }
}
}
class ItemViewHolder(val binding: ItemLicenseBinding) : RecyclerView.ViewHolder(binding.root)
}

View File

@ -1,33 +1,29 @@
package io.github.wulkanowy.ui.modules.about.license
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import android.view.ViewGroup
import androidx.appcompat.app.AlertDialog
import androidx.core.text.parseAsHtml
import androidx.recyclerview.widget.LinearLayoutManager
import com.mikepenz.aboutlibraries.Libs
import com.mikepenz.aboutlibraries.entity.Library
import dagger.Lazy
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import io.github.wulkanowy.R
import io.github.wulkanowy.databinding.FragmentLicenseBinding
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.setOnItemClickListener
import kotlinx.android.synthetic.main.fragment_license.*
import javax.inject.Inject
class LicenseFragment : BaseFragment(), LicenseView, MainView.TitledView {
class LicenseFragment : BaseFragment<FragmentLicenseBinding>(R.layout.fragment_license),
LicenseView, MainView.TitledView {
@Inject
lateinit var presenter: LicensePresenter
@Inject
lateinit var licenseAdapter: FlexibleAdapter<AbstractFlexibleItem<*>>
lateinit var licenseAdapter: LicenseAdapter
@Inject
lateinit var libs: Lazy<Libs>
@ -43,25 +39,26 @@ class LicenseFragment : BaseFragment(), LicenseView, MainView.TitledView {
fun newInstance() = LicenseFragment()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_license, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentLicenseBinding.bind(view)
presenter.onAttachView(this)
}
override fun initView() {
with(licenseRecycler) {
layoutManager = SmoothScrollLinearLayoutManager(context)
licenseAdapter.onClickListener = presenter::onItemSelected
with(binding.licenseRecycler) {
layoutManager = LinearLayoutManager(context)
adapter = licenseAdapter
}
licenseAdapter.setOnItemClickListener(presenter::onItemSelected)
}
override fun updateData(data: List<LicenseItem>) {
licenseAdapter.updateDataSet(data)
override fun updateData(data: List<Library>) {
with(licenseAdapter) {
items = data
notifyDataSetChanged()
}
}
override fun openLicense(licenseHtml: String) {
@ -76,7 +73,7 @@ class LicenseFragment : BaseFragment(), LicenseView, MainView.TitledView {
}
override fun showProgress(show: Boolean) {
licenseProgress.visibility = if (show) VISIBLE else GONE
binding.licenseProgress.visibility = if (show) VISIBLE else GONE
}
override fun onDestroyView() {

View File

@ -1,44 +0,0 @@
package io.github.wulkanowy.ui.modules.about.license
import android.view.View
import com.mikepenz.aboutlibraries.entity.Library
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import eu.davidea.flexibleadapter.items.IFlexible
import eu.davidea.viewholders.FlexibleViewHolder
import io.github.wulkanowy.R
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.item_license.*
class LicenseItem(val library: Library) : AbstractFlexibleItem<LicenseItem.ViewHolder>() {
override fun getLayoutRes() = R.layout.item_license
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<*>>) = ViewHolder(view, adapter)
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<*>>, holder: ViewHolder, position: Int, payloads: MutableList<Any>) {
with(holder) {
licenseItemName.text = library.libraryName
licenseItemSummary.text = library.license?.licenseName?.takeIf { it.isNotBlank() } ?: library.libraryVersion
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as LicenseItem
if (library != other.library) return false
return true
}
override fun hashCode() = library.hashCode()
class ViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<*>>) : FlexibleViewHolder(view, adapter),
LayoutContainer {
override val containerView: View? get() = contentView
}
}

View File

@ -1,6 +1,6 @@
package io.github.wulkanowy.ui.modules.about.license
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import com.mikepenz.aboutlibraries.entity.Library
import io.github.wulkanowy.data.repositories.student.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
@ -20,14 +20,12 @@ class LicensePresenter @Inject constructor(
loadData()
}
fun onItemSelected(item: AbstractFlexibleItem<*>) {
if (item !is LicenseItem) return
view?.run { item.library.license?.licenseDescription?.let { openLicense(it) } }
fun onItemSelected(library: Library) {
view?.run { library.license?.licenseDescription?.let { openLicense(it) } }
}
private fun loadData() {
disposable.add(Single.fromCallable { view?.appLibraries }
.map { it.map { library -> LicenseItem(library) } }
disposable.add(Single.fromCallable { view?.appLibraries.orEmpty() }
.subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread)
.doOnEvent { _, _ -> view?.showProgress(false) }

View File

@ -9,7 +9,7 @@ interface LicenseView : BaseView {
fun initView()
fun updateData(data: List<LicenseItem>)
fun updateData(data: List<Library>)
fun openLicense(licenseHtml: String)

View File

@ -8,23 +8,22 @@ import android.net.Uri
import android.os.Build.VERSION.SDK_INT
import android.os.Build.VERSION_CODES.LOLLIPOP
import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.core.content.FileProvider
import androidx.recyclerview.widget.LinearLayoutManager
import io.github.wulkanowy.BuildConfig.APPLICATION_ID
import io.github.wulkanowy.R
import io.github.wulkanowy.databinding.FragmentLogviewerBinding
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.main.MainView
import kotlinx.android.synthetic.main.fragment_logviewer.*
import java.io.File
import javax.inject.Inject
class LogViewerFragment : BaseFragment(), LogViewerView, MainView.TitledView {
class LogViewerFragment : BaseFragment<FragmentLogviewerBinding>(R.layout.fragment_logviewer),
LogViewerView, MainView.TitledView {
@Inject
lateinit var presenter: LogViewerPresenter
@ -43,13 +42,10 @@ class LogViewerFragment : BaseFragment(), LogViewerView, MainView.TitledView {
setHasOptionsMenu(true)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_logviewer, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
messageContainer = logViewerRecycler
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentLogviewerBinding.bind(view)
messageContainer = binding.logViewerRecycler
presenter.onAttachView(this)
}
@ -63,18 +59,18 @@ class LogViewerFragment : BaseFragment(), LogViewerView, MainView.TitledView {
}
override fun initView() {
with(logViewerRecycler) {
with(binding.logViewerRecycler) {
layoutManager = LinearLayoutManager(context)
adapter = logAdapter
}
logViewRefreshButton.setOnClickListener { presenter.onRefreshClick() }
binding.logViewRefreshButton.setOnClickListener { presenter.onRefreshClick() }
}
override fun setLines(lines: List<String>) {
logAdapter.lines = lines
logAdapter.notifyDataSetChanged()
logViewerRecycler.scrollToPosition(lines.size - 1)
binding.logViewerRecycler.scrollToPosition(lines.size - 1)
}
override fun shareLogs(files: List<File>) {

View File

@ -0,0 +1,46 @@
package io.github.wulkanowy.ui.modules.account
import android.annotation.SuppressLint
import android.graphics.PorterDuff
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
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 javax.inject.Inject
class AccountAdapter @Inject constructor() : RecyclerView.Adapter<AccountAdapter.ItemViewHolder>() {
var items = emptyList<Student>()
var onClickListener: (Student) -> Unit = {}
override fun getItemCount() = items.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ItemViewHolder(
ItemAccountBinding.inflate(LayoutInflater.from(parent.context), parent, false)
)
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
val student = items[position]
with(holder.binding) {
accountItemName.text = "${student.studentName} ${student.className}"
accountItemSchool.text = student.schoolName
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(student) }
}
}
class ItemViewHolder(val binding: ItemAccountBinding) : RecyclerView.ViewHolder(binding.root)
}

View File

@ -7,23 +7,21 @@ import android.view.ViewGroup
import android.widget.Toast
import android.widget.Toast.LENGTH_LONG
import androidx.appcompat.app.AlertDialog
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import androidx.recyclerview.widget.LinearLayoutManager
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.databinding.DialogAccountBinding
import io.github.wulkanowy.ui.base.BaseDialogFragment
import io.github.wulkanowy.ui.modules.login.LoginActivity
import io.github.wulkanowy.utils.setOnItemClickListener
import kotlinx.android.synthetic.main.dialog_account.*
import javax.inject.Inject
class AccountDialog : BaseDialogFragment(), AccountView {
class AccountDialog : BaseDialogFragment<DialogAccountBinding>(), AccountView {
@Inject
lateinit var presenter: AccountPresenter
@Inject
lateinit var accountAdapter: FlexibleAdapter<AbstractFlexibleItem<*>>
lateinit var accountAdapter: AccountAdapter
companion object {
fun newInstance() = AccountDialog()
@ -35,7 +33,7 @@ class AccountDialog : BaseDialogFragment(), AccountView {
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.dialog_account, container, false)
return DialogAccountBinding.inflate(inflater).apply { binding = this }.root
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
@ -44,18 +42,23 @@ class AccountDialog : BaseDialogFragment(), AccountView {
}
override fun initView() {
accountAdapter.setOnItemClickListener { presenter.onItemSelected(it) }
accountAdapter.onClickListener = presenter::onItemSelected
accountDialogAdd.setOnClickListener { presenter.onAddSelected() }
accountDialogRemove.setOnClickListener { presenter.onRemoveSelected() }
accountDialogRecycler.apply {
layoutManager = SmoothScrollLinearLayoutManager(context)
adapter = accountAdapter
with(binding) {
accountDialogAdd.setOnClickListener { presenter.onAddSelected() }
accountDialogRemove.setOnClickListener { presenter.onRemoveSelected() }
accountDialogRecycler.apply {
layoutManager = LinearLayoutManager(context)
adapter = accountAdapter
}
}
}
override fun updateData(data: List<AccountItem>) {
accountAdapter.updateDataSet(data)
override fun updateData(data: List<Student>) {
with(accountAdapter) {
items = data
notifyDataSetChanged()
}
}
override fun showError(text: String, error: Throwable) {

View File

@ -1,59 +0,0 @@
package io.github.wulkanowy.ui.modules.account
import android.annotation.SuppressLint
import android.graphics.PorterDuff
import android.view.View
import androidx.core.graphics.ColorUtils
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import eu.davidea.flexibleadapter.items.IFlexible
import eu.davidea.viewholders.FlexibleViewHolder
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.utils.getThemeAttrColor
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.item_account.*
class AccountItem(val student: Student) : AbstractFlexibleItem<AccountItem.ViewHolder>() {
override fun getLayoutRes() = R.layout.item_account
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<*>>) = ViewHolder(view, adapter)
@SuppressLint("SetTextI18n")
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<*>>, holder: ViewHolder, position: Int, payloads: MutableList<Any>?) {
val context = holder.contentView.context
val colorImage = if (student.isCurrent) context.getThemeAttrColor(R.attr.colorPrimary)
else ColorUtils.setAlphaComponent(context.getThemeAttrColor(R.attr.colorOnSurface), 153)
with(holder) {
accountItemName.text = "${student.studentName} ${student.className}"
accountItemSchool.text = student.schoolName
accountItemImage.setColorFilter(colorImage, PorterDuff.Mode.SRC_IN)
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as AccountItem
if (student != other.student) return false
return true
}
override fun hashCode(): Int {
var result = student.hashCode()
result = 31 * result + student.id.toInt()
return result
}
class ViewHolder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter),
LayoutContainer {
override val containerView: View? get() = contentView
}
}

View File

@ -1,6 +1,6 @@
package io.github.wulkanowy.ui.modules.account
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.repositories.student.StudentRepository
import io.github.wulkanowy.services.sync.SyncManager
import io.github.wulkanowy.ui.base.BasePresenter
@ -63,32 +63,29 @@ class AccountPresenter @Inject constructor(
}))
}
fun onItemSelected(item: AbstractFlexibleItem<*>) {
if (item is AccountItem) {
Timber.i("Select student item ${item.student.id}")
if (item.student.isCurrent) {
view?.dismissView()
} else {
Timber.i("Attempt to change a student")
disposable.add(studentRepository.switchStudent(item.student)
.subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread)
.doFinally { view?.dismissView() }
.subscribe({
Timber.i("Change a student result: Success")
view?.recreateMainView()
}, {
Timber.i("Change a student result: An exception occurred")
errorHandler.dispatch(it)
}))
}
fun onItemSelected(student: Student) {
Timber.i("Select student item ${student.id}")
if (student.isCurrent) {
view?.dismissView()
} else {
Timber.i("Attempt to change a student")
disposable.add(studentRepository.switchStudent(student)
.subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread)
.doFinally { view?.dismissView() }
.subscribe({
Timber.i("Change a student result: Success")
view?.recreateMainView()
}, {
Timber.i("Change a student result: An exception occurred")
errorHandler.dispatch(it)
}))
}
}
private fun loadData() {
Timber.i("Loading account data started")
disposable.add(studentRepository.getSavedStudents(false)
.map { it.map { item -> AccountItem(item) } }
.subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread)
.subscribe({
@ -100,4 +97,3 @@ class AccountPresenter @Inject constructor(
}))
}
}

View File

@ -1,12 +1,13 @@
package io.github.wulkanowy.ui.modules.account
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.ui.base.BaseView
interface AccountView : BaseView {
fun initView()
fun updateData(data: List<AccountItem>)
fun updateData(data: List<Student>)
fun dismissView()

View File

@ -1,12 +1,80 @@
package io.github.wulkanowy.ui.modules.attendance
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
import android.view.LayoutInflater
import android.view.View
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.Attendance
import io.github.wulkanowy.data.repositories.attendance.SentExcuseStatus
import io.github.wulkanowy.databinding.ItemAttendanceBinding
import javax.inject.Inject
class AttendanceAdapter<T : IFlexible<*>> : FlexibleAdapter<T>(null, null, true) {
class AttendanceAdapter @Inject constructor() :
RecyclerView.Adapter<AttendanceAdapter.ItemViewHolder>() {
var items = emptyList<Attendance>()
var excuseActionMode: Boolean = false
var onClickListener: (Attendance) -> Unit = {}
var onExcuseCheckboxSelect: (attendanceItem: Attendance, checked: Boolean) -> Unit = { _, _ -> }
override fun getItemCount() = items.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ItemViewHolder(
ItemAttendanceBinding.inflate(LayoutInflater.from(parent.context), parent, false)
)
override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
val item = items[position]
with(holder.binding) {
attendanceItemNumber.text = item.number.toString()
attendanceItemSubject.text = item.subject
attendanceItemDescription.text = item.name
attendanceItemAlert.visibility = item.run { if (absence && !excused) View.VISIBLE else View.INVISIBLE }
attendanceItemNumber.visibility = View.GONE
attendanceItemExcuseInfo.visibility = View.GONE
attendanceItemExcuseCheckbox.visibility = View.GONE
attendanceItemExcuseCheckbox.isChecked = false
attendanceItemExcuseCheckbox.setOnCheckedChangeListener { _, checked ->
onExcuseCheckboxSelect(item, checked)
}
when (if (item.excuseStatus != null) SentExcuseStatus.valueOf(item.excuseStatus) else null) {
SentExcuseStatus.WAITING -> {
attendanceItemExcuseInfo.setImageResource(R.drawable.ic_excuse_waiting)
attendanceItemExcuseInfo.visibility = View.VISIBLE
attendanceItemAlert.visibility = View.INVISIBLE
}
SentExcuseStatus.DENIED -> {
attendanceItemExcuseInfo.setImageResource(R.drawable.ic_excuse_denied)
attendanceItemExcuseInfo.visibility = View.VISIBLE
}
else -> {
if (item.excusable && excuseActionMode) {
attendanceItemNumber.visibility = View.GONE
attendanceItemExcuseCheckbox.visibility = View.VISIBLE
} else {
attendanceItemNumber.visibility = View.VISIBLE
attendanceItemExcuseCheckbox.visibility = View.GONE
}
}
}
root.setOnClickListener {
onClickListener(item)
with(attendanceItemExcuseCheckbox) {
if (excuseActionMode && isVisible) {
isChecked = !isChecked
}
}
}
}
}
class ItemViewHolder(val binding: ItemAttendanceBinding) : RecyclerView.ViewHolder(binding.root)
}

View File

@ -5,13 +5,15 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.DialogFragment
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Attendance
import io.github.wulkanowy.databinding.DialogAttendanceBinding
import io.github.wulkanowy.utils.lifecycleAwareVariable
import io.github.wulkanowy.utils.toFormattedString
import kotlinx.android.synthetic.main.dialog_attendance.*
class AttendanceDialog : DialogFragment() {
private var binding: DialogAttendanceBinding by lifecycleAwareVariable()
private lateinit var attendance: Attendance
companion object {
@ -33,16 +35,18 @@ class AttendanceDialog : DialogFragment() {
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.dialog_attendance, container, false)
return DialogAttendanceBinding.inflate(inflater).apply { binding = this }.root
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
attendanceDialogSubject.text = attendance.subject
attendanceDialogDescription.text = attendance.name
attendanceDialogDate.text = attendance.date.toFormattedString()
attendanceDialogNumber.text = attendance.number.toString()
attendanceDialogClose.setOnClickListener { dismiss() }
with(binding) {
attendanceDialogSubject.text = attendance.subject
attendanceDialogDescription.text = attendance.name
attendanceDialogDate.text = attendance.date.toFormattedString()
attendanceDialogNumber.text = attendance.number.toString()
attendanceDialogClose.setOnClickListener { dismiss() }
}
}
}

View File

@ -10,35 +10,32 @@ import android.view.View
import android.view.View.GONE
import android.view.View.INVISIBLE
import android.view.View.VISIBLE
import android.view.ViewGroup
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.view.ActionMode
import androidx.recyclerview.widget.LinearLayoutManager
import com.wdullaer.materialdatetimepicker.date.DatePickerDialog
import eu.davidea.flexibleadapter.common.FlexibleItemDecoration
import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Attendance
import io.github.wulkanowy.databinding.DialogExcuseBinding
import io.github.wulkanowy.databinding.FragmentAttendanceBinding
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.widgets.DividerItemDecoration
import io.github.wulkanowy.ui.modules.attendance.summary.AttendanceSummaryFragment
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.SchooldaysRangeLimiter
import io.github.wulkanowy.utils.dpToPx
import io.github.wulkanowy.utils.setOnItemClickListener
import kotlinx.android.synthetic.main.dialog_excuse.*
import kotlinx.android.synthetic.main.fragment_attendance.*
import org.threeten.bp.LocalDate
import javax.inject.Inject
class AttendanceFragment : BaseFragment(), AttendanceView, MainView.MainChildView,
class AttendanceFragment : BaseFragment<FragmentAttendanceBinding>(R.layout.fragment_attendance), AttendanceView, MainView.MainChildView,
MainView.TitledView {
@Inject
lateinit var presenter: AttendancePresenter
@Inject
lateinit var attendanceAdapter: AttendanceAdapter<AbstractFlexibleItem<*>>
lateinit var attendanceAdapter: AttendanceAdapter
override val excuseSuccessString: String
get() = getString(R.string.attendance_excuse_success)
@ -54,7 +51,7 @@ class AttendanceFragment : BaseFragment(), AttendanceView, MainView.MainChildVie
override val titleStringId get() = R.string.attendance_title
override val isViewEmpty get() = attendanceAdapter.isEmpty
override val isViewEmpty get() = attendanceAdapter.items.isEmpty()
override val currentStackSize get() = (activity as? MainActivity)?.currentStackSize
@ -91,39 +88,38 @@ class AttendanceFragment : BaseFragment(), AttendanceView, MainView.MainChildVie
setHasOptionsMenu(true)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_attendance, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
messageContainer = attendanceRecycler
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentAttendanceBinding.bind(view)
messageContainer = binding.attendanceRecycler
presenter.onAttachView(this, savedInstanceState?.getLong(SAVED_DATE_KEY))
}
override fun initView() {
attendanceAdapter.setOnItemClickListener(presenter::onAttendanceItemSelected)
attendanceAdapter.onExcuseCheckboxSelect = presenter::onExcuseCheckboxSelect
with(attendanceRecycler) {
layoutManager = SmoothScrollLinearLayoutManager(context)
adapter = attendanceAdapter
addItemDecoration(FlexibleItemDecoration(context)
.withDefaultDivider()
.withDrawDividerOnLastItem(false))
with(attendanceAdapter) {
onClickListener = presenter::onAttendanceItemSelected
onExcuseCheckboxSelect = presenter::onExcuseCheckboxSelect
}
attendanceSwipe.setOnRefreshListener(presenter::onSwipeRefresh)
attendanceErrorRetry.setOnClickListener { presenter.onRetry() }
attendanceErrorDetails.setOnClickListener { presenter.onDetailsClick() }
with(binding.attendanceRecycler) {
layoutManager = LinearLayoutManager(context)
adapter = attendanceAdapter
addItemDecoration(DividerItemDecoration(context))
}
attendancePreviousButton.setOnClickListener { presenter.onPreviousDay() }
attendanceNavDate.setOnClickListener { presenter.onPickDate() }
attendanceNextButton.setOnClickListener { presenter.onNextDay() }
with(binding) {
attendanceSwipe.setOnRefreshListener(presenter::onSwipeRefresh)
attendanceErrorRetry.setOnClickListener { presenter.onRetry() }
attendanceErrorDetails.setOnClickListener { presenter.onDetailsClick() }
attendanceExcuseButton.setOnClickListener { presenter.onExcuseButtonClick() }
attendancePreviousButton.setOnClickListener { presenter.onPreviousDay() }
attendanceNavDate.setOnClickListener { presenter.onPickDate() }
attendanceNextButton.setOnClickListener { presenter.onNextDay() }
attendanceNavContainer.setElevationCompat(requireContext().dpToPx(8f))
attendanceExcuseButton.setOnClickListener { presenter.onExcuseButtonClick() }
attendanceNavContainer.setElevationCompat(requireContext().dpToPx(8f))
}
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
@ -135,20 +131,26 @@ class AttendanceFragment : BaseFragment(), AttendanceView, MainView.MainChildVie
else false
}
override fun updateData(data: List<AttendanceItem>) {
attendanceAdapter.updateDataSet(data, true)
override fun updateData(data: List<Attendance>) {
with(attendanceAdapter) {
items = data
notifyDataSetChanged()
}
}
override fun updateNavigationDay(date: String) {
attendanceNavDate.text = date
binding.attendanceNavDate.text = date
}
override fun clearData() {
attendanceAdapter.clear()
with(attendanceAdapter) {
items = emptyList()
notifyDataSetChanged()
}
}
override fun resetView() {
attendanceRecycler.smoothScrollToPosition(0)
binding.attendanceRecycler.smoothScrollToPosition(0)
}
override fun onFragmentReselected() {
@ -164,43 +166,43 @@ class AttendanceFragment : BaseFragment(), AttendanceView, MainView.MainChildVie
}
override fun showEmpty(show: Boolean) {
attendanceEmpty.visibility = if (show) VISIBLE else GONE
binding.attendanceEmpty.visibility = if (show) VISIBLE else GONE
}
override fun showErrorView(show: Boolean) {
attendanceError.visibility = if (show) VISIBLE else GONE
binding.attendanceError.visibility = if (show) VISIBLE else GONE
}
override fun setErrorDetails(message: String) {
attendanceErrorMessage.text = message
binding.attendanceErrorMessage.text = message
}
override fun showProgress(show: Boolean) {
attendanceProgress.visibility = if (show) VISIBLE else GONE
binding.attendanceProgress.visibility = if (show) VISIBLE else GONE
}
override fun enableSwipe(enable: Boolean) {
attendanceSwipe.isEnabled = enable
binding.attendanceSwipe.isEnabled = enable
}
override fun showContent(show: Boolean) {
attendanceRecycler.visibility = if (show) VISIBLE else GONE
binding. attendanceRecycler.visibility = if (show) VISIBLE else GONE
}
override fun hideRefresh() {
attendanceSwipe.isRefreshing = false
binding.attendanceSwipe.isRefreshing = false
}
override fun showPreButton(show: Boolean) {
attendancePreviousButton.visibility = if (show) VISIBLE else INVISIBLE
binding.attendancePreviousButton.visibility = if (show) VISIBLE else INVISIBLE
}
override fun showNextButton(show: Boolean) {
attendanceNextButton.visibility = if (show) VISIBLE else INVISIBLE
binding. attendanceNextButton.visibility = if (show) VISIBLE else INVISIBLE
}
override fun showExcuseButton(show: Boolean) {
attendanceExcuseButton.visibility = if (show) VISIBLE else GONE
binding.attendanceExcuseButton.visibility = if (show) VISIBLE else GONE
}
override fun showAttendanceDialog(lesson: Attendance) {
@ -223,14 +225,15 @@ class AttendanceFragment : BaseFragment(), AttendanceView, MainView.MainChildVie
}
override fun showExcuseDialog() {
val dialogBinding = DialogExcuseBinding.inflate(LayoutInflater.from(context))
AlertDialog.Builder(requireContext())
.setTitle(R.string.attendance_excuse_title)
.setView(R.layout.dialog_excuse)
.setView(dialogBinding.root)
.setNegativeButton(android.R.string.cancel) { _, _ -> }
.create()
.apply {
setButton(BUTTON_POSITIVE, getString(R.string.attendance_excuse_dialog_submit)) { _, _ ->
presenter.onExcuseDialogSubmit(excuseReason.text?.toString().orEmpty())
presenter.onExcuseDialogSubmit(dialogBinding.excuseReason.text?.toString().orEmpty())
}
}.show()
}

View File

@ -1,97 +0,0 @@
package io.github.wulkanowy.ui.modules.attendance
import android.view.View
import android.view.View.GONE
import android.view.View.INVISIBLE
import android.view.View.VISIBLE
import androidx.core.view.isVisible
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import eu.davidea.flexibleadapter.items.IFlexible
import eu.davidea.viewholders.FlexibleViewHolder
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Attendance
import io.github.wulkanowy.data.repositories.attendance.SentExcuseStatus
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.item_attendance.*
class AttendanceItem(val attendance: Attendance) :
AbstractFlexibleItem<AttendanceItem.ViewHolder>() {
override fun getLayoutRes() = R.layout.item_attendance
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<*>>): ViewHolder {
return ViewHolder(view, adapter)
}
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<*>>, holder: ViewHolder, position: Int, payloads: MutableList<Any>?) {
holder.apply {
attendanceItemNumber.text = attendance.number.toString()
attendanceItemSubject.text = attendance.subject
attendanceItemDescription.text = attendance.name
attendanceItemAlert.visibility = attendance.run { if (absence && !excused) VISIBLE else INVISIBLE }
attendanceItemNumber.visibility = GONE
attendanceItemExcuseInfo.visibility = GONE
attendanceItemExcuseCheckbox.visibility = GONE
attendanceItemExcuseCheckbox.isChecked = false
attendanceItemExcuseCheckbox.setOnCheckedChangeListener { _, checked ->
(adapter as AttendanceAdapter).onExcuseCheckboxSelect(attendance, checked)
}
when (if (attendance.excuseStatus != null) SentExcuseStatus.valueOf(attendance.excuseStatus) else null) {
SentExcuseStatus.WAITING -> {
attendanceItemExcuseInfo.setImageResource(R.drawable.ic_excuse_waiting)
attendanceItemExcuseInfo.visibility = VISIBLE
attendanceItemAlert.visibility = INVISIBLE
}
SentExcuseStatus.DENIED -> {
attendanceItemExcuseInfo.setImageResource(R.drawable.ic_excuse_denied)
attendanceItemExcuseInfo.visibility = VISIBLE
}
else -> {
if (attendance.excusable && (adapter as AttendanceAdapter).excuseActionMode) {
attendanceItemNumber.visibility = GONE
attendanceItemExcuseCheckbox.visibility = VISIBLE
} else {
attendanceItemNumber.visibility = VISIBLE
attendanceItemExcuseCheckbox.visibility = GONE
}
}
}
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as AttendanceItem
if (attendance != other.attendance) return false
return true
}
override fun hashCode(): Int {
var result = attendance.hashCode()
result = 31 * result + attendance.id.toInt()
return result
}
class ViewHolder(view: View, val adapter: FlexibleAdapter<*>) :
FlexibleViewHolder(view, adapter),
LayoutContainer {
override val containerView: View
get() = contentView
override fun onClick(view: View?) {
super.onClick(view)
attendanceItemExcuseCheckbox.apply {
if ((adapter as AttendanceAdapter).excuseActionMode && isVisible) {
isChecked = !isChecked
}
}
}
}
}

View File

@ -1,12 +0,0 @@
package io.github.wulkanowy.ui.modules.attendance
import dagger.Module
import dagger.Provides
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
@Module
class AttendanceModule {
@Provides
fun provideAttendanceFlexibleAdapter() = AttendanceAdapter<AbstractFlexibleItem<*>>()
}

View File

@ -1,7 +1,6 @@
package io.github.wulkanowy.ui.modules.attendance
import android.annotation.SuppressLint
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import io.github.wulkanowy.data.db.entities.Attendance
import io.github.wulkanowy.data.repositories.attendance.AttendanceRepository
import io.github.wulkanowy.data.repositories.preferences.PreferencesRepository
@ -111,11 +110,11 @@ class AttendancePresenter @Inject constructor(
view?.finishActionMode()
}
fun onAttendanceItemSelected(item: AbstractFlexibleItem<*>?) {
fun onAttendanceItemSelected(attendance: Attendance) {
view?.apply {
if (item is AttendanceItem && !excuseActionMode) {
Timber.i("Select attendance item ${item.attendance.id}")
showAttendanceDialog(item.attendance)
if (!excuseActionMode) {
Timber.i("Select attendance item ${attendance.id}")
showAttendanceDialog(attendance)
}
}
}
@ -197,9 +196,7 @@ class AttendancePresenter @Inject constructor(
if (prefRepository.isShowPresent) list
else list.filter { !it.presence }
}
.delay(200, MILLISECONDS)
.map { items -> items.map { AttendanceItem(it) } }
.map { items -> items.sortedBy { it.attendance.number } }
.map { items -> items.sortedBy { it.number } }
.subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread)
.doFinally {
@ -216,9 +213,14 @@ class AttendancePresenter @Inject constructor(
showEmpty(it.isEmpty())
showErrorView(false)
showContent(it.isNotEmpty())
showExcuseButton(it.any { item -> item.attendance.excusable })
showExcuseButton(it.any { item -> item.excusable })
}
analytics.logEvent("load_attendance", "items" to it.size, "force_refresh" to forceRefresh)
analytics.logEvent(
"load_data",
"type" to "attendance",
"items" to it.size,
"force_refresh" to forceRefresh
)
}) {
Timber.i("Loading attendance result: An exception occurred")
errorHandler.dispatch(it)
@ -236,7 +238,6 @@ class AttendancePresenter @Inject constructor(
attendanceRepository.excuseForAbsence(student, semester, toExcuseList, reason)
}
}
.delay(200, MILLISECONDS)
.subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread)
.doOnSubscribe {

View File

@ -18,7 +18,7 @@ interface AttendanceView : BaseView {
fun initView()
fun updateData(data: List<AttendanceItem>)
fun updateData(data: List<Attendance>)
fun updateNavigationDay(date: String)

View File

@ -0,0 +1,101 @@
package io.github.wulkanowy.ui.modules.attendance.summary
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.AttendanceSummary
import io.github.wulkanowy.databinding.ItemAttendanceSummaryBinding
import io.github.wulkanowy.databinding.ScrollableHeaderAttendanceSummaryBinding
import io.github.wulkanowy.utils.calculatePercentage
import io.github.wulkanowy.utils.getFormattedName
import org.threeten.bp.Month
import java.util.Locale
import javax.inject.Inject
class AttendanceSummaryAdapter @Inject constructor() :
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private enum class ViewType(val id: Int) {
HEADER(1),
ITEM(2)
}
var items = emptyList<AttendanceSummary>()
override fun getItemCount() = if (items.isNotEmpty()) items.size + 2 else 0
override fun getItemViewType(position: Int) = when (position) {
0 -> ViewType.HEADER.id
else -> ViewType.ITEM.id
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context)
return when (viewType) {
ViewType.HEADER.id -> HeaderViewHolder(ScrollableHeaderAttendanceSummaryBinding.inflate(inflater, parent, false))
ViewType.ITEM.id -> ItemViewHolder(ItemAttendanceSummaryBinding.inflate(inflater, parent, false))
else -> throw IllegalStateException()
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is HeaderViewHolder -> bindHeaderViewHolder(holder.binding)
is ItemViewHolder -> bindItemViewHolder(holder.binding, position - 2)
}
}
private fun bindHeaderViewHolder(binding: ScrollableHeaderAttendanceSummaryBinding) {
binding.attendanceSummaryScrollableHeaderPercentage.text = formatPercentage(items.calculatePercentage())
}
private fun bindItemViewHolder(binding: ItemAttendanceSummaryBinding, position: Int) {
val item = if (position == -1) getTotalItem() else items[position]
with(binding) {
attendanceSummaryMonth.text = when (position) {
-1 -> root.context.getString(R.string.attendance_summary_total)
else -> item.month.getFormattedName()
}
attendanceSummaryPercentage.text = when (position) {
-1 -> formatPercentage(items.calculatePercentage())
else -> formatPercentage(item.calculatePercentage())
}
attendanceSummaryPresent.text = item.presence.toString()
attendanceSummaryAbsenceUnexcused.text = item.absence.toString()
attendanceSummaryAbsenceExcused.text = item.absenceExcused.toString()
attendanceSummaryAbsenceSchool.text = item.absenceForSchoolReasons.toString()
attendanceSummaryExemption.text = item.exemption.toString()
attendanceSummaryLatenessUnexcused.text = item.lateness.toString()
attendanceSummaryLatenessExcused.text = item.latenessExcused.toString()
}
}
private fun getTotalItem() = AttendanceSummary(
month = Month.APRIL,
presence = items.sumBy { it.presence },
absence = items.sumBy { it.absence },
absenceExcused = items.sumBy { it.absenceExcused },
absenceForSchoolReasons = items.sumBy { it.absenceForSchoolReasons },
exemption = items.sumBy { it.exemption },
lateness = items.sumBy { it.lateness },
latenessExcused = items.sumBy { it.latenessExcused },
diaryId = -1,
studentId = -1,
subjectId = -1
)
private fun formatPercentage(percentage: Double): String {
return if (percentage == 0.0) "0%"
else "${String.format(Locale.FRANCE, "%.2f", percentage)}%"
}
class HeaderViewHolder(val binding: ScrollableHeaderAttendanceSummaryBinding) :
RecyclerView.ViewHolder(binding.root)
class ItemViewHolder(val binding: ItemAttendanceSummaryBinding) :
RecyclerView.ViewHolder(binding.root)
}

View File

@ -1,32 +1,31 @@
package io.github.wulkanowy.ui.modules.attendance.summary
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.View.GONE
import android.view.View.INVISIBLE
import android.view.View.VISIBLE
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.TextView
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import androidx.recyclerview.widget.LinearLayoutManager
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.AttendanceSummary
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.setOnItemSelectedListener
import kotlinx.android.synthetic.main.fragment_attendance_summary.*
import javax.inject.Inject
class AttendanceSummaryFragment : BaseFragment(), AttendanceSummaryView, MainView.TitledView {
class AttendanceSummaryFragment :
BaseFragment<FragmentAttendanceSummaryBinding>(R.layout.fragment_attendance_summary),
AttendanceSummaryView, MainView.TitledView {
@Inject
lateinit var presenter: AttendanceSummaryPresenter
@Inject
lateinit var attendanceSummaryAdapter: FlexibleAdapter<AbstractFlexibleItem<*>>
lateinit var attendanceSummaryAdapter: AttendanceSummaryAdapter
private lateinit var subjectsAdapter: ArrayAdapter<String>
@ -36,41 +35,38 @@ class AttendanceSummaryFragment : BaseFragment(), AttendanceSummaryView, MainVie
fun newInstance() = AttendanceSummaryFragment()
}
override val totalString get() = getString(R.string.attendance_summary_total)
override val titleStringId get() = R.string.attendance_title
override val isViewEmpty get() = attendanceSummaryAdapter.isEmpty
override val isViewEmpty get() = attendanceSummaryAdapter.items.isEmpty()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_attendance_summary, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
messageContainer = attendanceSummaryRecycler
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentAttendanceSummaryBinding.bind(view)
messageContainer = binding.attendanceSummaryRecycler
presenter.onAttachView(this, savedInstanceState?.getInt(SAVED_SUBJECT_KEY))
}
override fun initView() {
with(attendanceSummaryRecycler) {
layoutManager = SmoothScrollLinearLayoutManager(context)
with(binding.attendanceSummaryRecycler) {
layoutManager = LinearLayoutManager(context)
adapter = attendanceSummaryAdapter
}
attendanceSummarySwipe.setOnRefreshListener(presenter::onSwipeRefresh)
attendanceSummaryErrorRetry.setOnClickListener { presenter.onRetry() }
attendanceSummaryErrorDetails.setOnClickListener { presenter.onDetailsClick() }
with(binding) {
attendanceSummarySwipe.setOnRefreshListener(presenter::onSwipeRefresh)
attendanceSummaryErrorRetry.setOnClickListener { presenter.onRetry() }
attendanceSummaryErrorDetails.setOnClickListener { presenter.onDetailsClick() }
}
subjectsAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, mutableListOf())
subjectsAdapter.setDropDownViewResource(R.layout.item_attendance_summary_subject)
with(attendanceSummarySubjects) {
with(binding.attendanceSummarySubjects) {
adapter = subjectsAdapter
setOnItemSelectedListener<TextView> { presenter.onSubjectSelected(it?.text?.toString()) }
}
attendanceSummarySubjectsContainer.setElevationCompat(requireContext().dpToPx(1f))
binding.attendanceSummarySubjectsContainer.setElevationCompat(requireContext().dpToPx(1f))
}
override fun updateSubjects(data: ArrayList<String>) {
@ -81,48 +77,50 @@ class AttendanceSummaryFragment : BaseFragment(), AttendanceSummaryView, MainVie
}
}
override fun updateDataSet(data: List<AttendanceSummaryItem>, header: AttendanceSummaryScrollableHeader) {
override fun updateDataSet(data: List<AttendanceSummary>) {
with(attendanceSummaryAdapter) {
updateDataSet(data, true)
removeAllScrollableHeaders()
addScrollableHeader(header)
items = data
notifyDataSetChanged()
}
}
override fun clearView() {
attendanceSummaryAdapter.clear()
with(attendanceSummaryAdapter) {
items = emptyList()
notifyDataSetChanged()
}
}
override fun showEmpty(show: Boolean) {
attendanceSummaryEmpty.visibility = if (show) VISIBLE else GONE
binding.attendanceSummaryEmpty.visibility = if (show) VISIBLE else GONE
}
override fun showErrorView(show: Boolean) {
attendanceSummaryError.visibility = if (show) VISIBLE else GONE
binding.attendanceSummaryError.visibility = if (show) VISIBLE else GONE
}
override fun setErrorDetails(message: String) {
attendanceSummaryErrorMessage.text = message
binding.attendanceSummaryErrorMessage.text = message
}
override fun showProgress(show: Boolean) {
attendanceSummaryProgress.visibility = if (show) VISIBLE else GONE
binding.attendanceSummaryProgress.visibility = if (show) VISIBLE else GONE
}
override fun enableSwipe(enable: Boolean) {
attendanceSummarySwipe.isEnabled = enable
binding.attendanceSummarySwipe.isEnabled = enable
}
override fun showContent(show: Boolean) {
attendanceSummaryRecycler.visibility = if (show) VISIBLE else GONE
binding.attendanceSummaryRecycler.visibility = if (show) VISIBLE else GONE
}
override fun showSubjects(show: Boolean) {
attendanceSummarySubjectsContainer.visibility = if (show) VISIBLE else INVISIBLE
binding.attendanceSummarySubjectsContainer.visibility = if (show) VISIBLE else INVISIBLE
}
override fun hideRefresh() {
attendanceSummarySwipe.isRefreshing = false
binding.attendanceSummarySwipe.isRefreshing = false
}
override fun onSaveInstanceState(outState: Bundle) {
@ -131,7 +129,7 @@ class AttendanceSummaryFragment : BaseFragment(), AttendanceSummaryView, MainVie
}
override fun onDestroyView() {
super.onDestroyView()
presenter.onDetachView()
super.onDestroyView()
}
}

View File

@ -1,80 +0,0 @@
package io.github.wulkanowy.ui.modules.attendance.summary
import android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import eu.davidea.flexibleadapter.items.IFlexible
import eu.davidea.viewholders.FlexibleViewHolder
import io.github.wulkanowy.R
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.item_attendance_summary.*
class AttendanceSummaryItem(
private val month: String,
private val percentage: String,
private val present: String,
private val absence: String,
private val excusedAbsence: String,
private val schoolAbsence: String,
private val exemption: String,
private val lateness: String,
private val excusedLateness: String
) : AbstractFlexibleItem<AttendanceSummaryItem.ViewHolder>() {
override fun getLayoutRes() = R.layout.item_attendance_summary
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<*>>): ViewHolder {
return ViewHolder(view, adapter)
}
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<*>>, holder: ViewHolder, position: Int, payloads: MutableList<Any>?) {
holder.apply {
attendanceSummaryMonth.text = month
attendanceSummaryPercentage.text = percentage
attendanceSummaryPresent.text = present
attendanceSummaryAbsenceUnexcused.text = absence
attendanceSummaryAbsenceExcused.text = excusedAbsence
attendanceSummaryAbsenceSchool.text = schoolAbsence
attendanceSummaryExemption.text = exemption
attendanceSummaryLatenessUnexcused.text = lateness
attendanceSummaryLatenessExcused.text = excusedLateness
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as AttendanceSummaryItem
if (month != other.month) return false
if (percentage != other.percentage) return false
if (present != other.present) return false
if (absence != other.absence) return false
if (excusedAbsence != other.excusedAbsence) return false
if (schoolAbsence != other.schoolAbsence) return false
if (exemption != other.exemption) return false
if (lateness != other.lateness) return false
if (excusedLateness != other.excusedLateness) return false
return true
}
override fun hashCode(): Int {
var result = month.hashCode()
result = 31 * result + percentage.hashCode()
result = 31 * result + present.hashCode()
result = 31 * result + absence.hashCode()
result = 31 * result + excusedAbsence.hashCode()
result = 31 * result + schoolAbsence.hashCode()
result = 31 * result + exemption.hashCode()
result = 31 * result + lateness.hashCode()
result = 31 * result + excusedLateness.hashCode()
return result
}
class ViewHolder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter), LayoutContainer {
override val containerView: View
get() = contentView
}
}

View File

@ -1,6 +1,5 @@
package io.github.wulkanowy.ui.modules.attendance.summary
import io.github.wulkanowy.data.db.entities.AttendanceSummary
import io.github.wulkanowy.data.db.entities.Subject
import io.github.wulkanowy.data.repositories.attendancesummary.AttendanceSummaryRepository
import io.github.wulkanowy.data.repositories.semester.SemesterRepository
@ -10,13 +9,8 @@ import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.utils.FirebaseAnalyticsHelper
import io.github.wulkanowy.utils.SchedulersProvider
import io.github.wulkanowy.utils.calculatePercentage
import io.github.wulkanowy.utils.getFormattedName
import org.threeten.bp.Month
import timber.log.Timber
import java.lang.String.format
import java.util.Locale.FRANCE
import java.util.concurrent.TimeUnit.MILLISECONDS
import javax.inject.Inject
class AttendanceSummaryPresenter @Inject constructor(
@ -88,8 +82,7 @@ class AttendanceSummaryPresenter @Inject constructor(
attendanceSummaryRepository.getAttendanceSummary(student, it, subjectId, forceRefresh)
}
}
.map { createAttendanceSummaryItems(it) to AttendanceSummaryScrollableHeader(formatPercentage(it.calculatePercentage())) }
.delay(200, MILLISECONDS)
.map { items -> items.sortedByDescending { if (it.month.value <= Month.JUNE.value) it.month.value + 12 else it.month.value } }
.subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread)
.doFinally {
@ -102,11 +95,17 @@ class AttendanceSummaryPresenter @Inject constructor(
.subscribe({
Timber.i("Loading attendance summary result: Success")
view?.apply {
showEmpty(it.first.isEmpty())
showContent(it.first.isNotEmpty())
updateDataSet(it.first, it.second)
showEmpty(it.isEmpty())
showContent(it.isNotEmpty())
updateDataSet(it)
}
analytics.logEvent("load_attendance_summary", "items" to it.first.size, "force_refresh" to forceRefresh, "item_id" to subjectId)
analytics.logEvent(
"load_data",
"type" to "attendance_summary",
"items" to it.size,
"force_refresh" to forceRefresh,
"item_id" to subjectId
)
}) {
Timber.i("Loading attendance summary result: An exception occurred")
errorHandler.dispatch(it)
@ -150,42 +149,4 @@ class AttendanceSummaryPresenter @Inject constructor(
})
)
}
private fun createAttendanceSummaryTotalItem(attendanceSummary: List<AttendanceSummary>): AttendanceSummaryItem {
return AttendanceSummaryItem(
month = view?.totalString.orEmpty(),
percentage = formatPercentage(attendanceSummary.calculatePercentage()),
present = attendanceSummary.sumBy { it.presence }.toString(),
absence = attendanceSummary.sumBy { it.absence }.toString(),
excusedAbsence = attendanceSummary.sumBy { it.absenceExcused }.toString(),
schoolAbsence = attendanceSummary.sumBy { it.absenceForSchoolReasons }.toString(),
exemption = attendanceSummary.sumBy { it.exemption }.toString(),
lateness = attendanceSummary.sumBy { it.lateness }.toString(),
excusedLateness = attendanceSummary.sumBy { it.latenessExcused }.toString()
)
}
private fun createAttendanceSummaryItems(attendanceSummary: List<AttendanceSummary>): List<AttendanceSummaryItem> {
if (attendanceSummary.isEmpty()) return emptyList()
return listOf(createAttendanceSummaryTotalItem(attendanceSummary)) + attendanceSummary.sortedByDescending {
if (it.month.value <= Month.JUNE.value) it.month.value + 12 else it.month.value
}.map {
AttendanceSummaryItem(
month = it.month.getFormattedName(),
percentage = formatPercentage(it.calculatePercentage()),
present = it.presence.toString(),
absence = it.absence.toString(),
excusedAbsence = it.absenceExcused.toString(),
schoolAbsence = it.absenceForSchoolReasons.toString(),
exemption = it.exemption.toString(),
lateness = it.lateness.toString(),
excusedLateness = it.latenessExcused.toString()
)
}
}
private fun formatPercentage(percentage: Double): String {
return if (percentage == 0.0) "0%"
else "${format(FRANCE, "%.2f", percentage)}%"
}
}

View File

@ -1,46 +0,0 @@
package io.github.wulkanowy.ui.modules.attendance.summary
import android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import eu.davidea.flexibleadapter.items.IFlexible
import eu.davidea.viewholders.FlexibleViewHolder
import io.github.wulkanowy.R
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.scrollable_header_attendance_summary.*
class AttendanceSummaryScrollableHeader(private val percentage: String) :
AbstractFlexibleItem<AttendanceSummaryScrollableHeader.ViewHolder>() {
override fun getLayoutRes() = R.layout.scrollable_header_attendance_summary
override fun createViewHolder(view: View?, adapter: FlexibleAdapter<IFlexible<*>>?): ViewHolder {
return ViewHolder(view, adapter)
}
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<*>>?, holder: ViewHolder?, position: Int, payloads: MutableList<Any>?) {
holder?.apply { attendanceSummaryScrollableHeaderPercentage.text = percentage }
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as AttendanceSummaryScrollableHeader
if (percentage != other.percentage) return false
return true
}
override fun hashCode(): Int {
return percentage.hashCode()
}
class ViewHolder(view: View?, adapter: FlexibleAdapter<IFlexible<*>>?) : FlexibleViewHolder(view, adapter),
LayoutContainer {
override val containerView: View?
get() = contentView
}
}

View File

@ -1,11 +1,10 @@
package io.github.wulkanowy.ui.modules.attendance.summary
import io.github.wulkanowy.data.db.entities.AttendanceSummary
import io.github.wulkanowy.ui.base.BaseView
interface AttendanceSummaryView : BaseView {
val totalString: String
val isViewEmpty: Boolean
fun initView()
@ -24,7 +23,7 @@ interface AttendanceSummaryView : BaseView {
fun setErrorDetails(message: String)
fun updateDataSet(data: List<AttendanceSummaryItem>, header: AttendanceSummaryScrollableHeader)
fun updateDataSet(data: List<AttendanceSummary>)
fun updateSubjects(data: ArrayList<String>)

View File

@ -0,0 +1,65 @@
package io.github.wulkanowy.ui.modules.exam
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import io.github.wulkanowy.data.db.entities.Exam
import io.github.wulkanowy.databinding.HeaderExamBinding
import io.github.wulkanowy.databinding.ItemExamBinding
import io.github.wulkanowy.utils.toFormattedString
import io.github.wulkanowy.utils.weekDayName
import org.threeten.bp.LocalDate
import javax.inject.Inject
class ExamAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
var items = emptyList<ExamItem<*>>()
var onClickListener: (Exam) -> Unit = {}
override fun getItemCount() = items.size
override fun getItemViewType(position: Int) = items[position].viewType.id
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context)
return when (viewType) {
ExamItem.ViewType.HEADER.id -> HeaderViewHolder(HeaderExamBinding.inflate(inflater, parent, false))
ExamItem.ViewType.ITEM.id -> ItemViewHolder(ItemExamBinding.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 LocalDate)
is ItemViewHolder -> bindItemViewHolder(holder.binding, items[position].value as Exam)
}
}
@SuppressLint("DefaultLocale")
private fun bindHeaderViewHolder(binding: HeaderExamBinding, date: LocalDate) {
with(binding) {
examHeaderDay.text = date.weekDayName.capitalize()
examHeaderDate.text = date.toFormattedString()
}
}
private fun bindItemViewHolder(binding: ItemExamBinding, exam: Exam) {
with(binding) {
examItemSubject.text = exam.subject
examItemTeacher.text = exam.teacher
examItemType.text = exam.type
root.setOnClickListener { onClickListener(exam) }
}
}
private class HeaderViewHolder(val binding: HeaderExamBinding) :
RecyclerView.ViewHolder(binding.root)
private class ItemViewHolder(val binding: ItemExamBinding) :
RecyclerView.ViewHolder(binding.root)
}

View File

@ -5,13 +5,15 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.DialogFragment
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Exam
import io.github.wulkanowy.databinding.DialogExamBinding
import io.github.wulkanowy.utils.lifecycleAwareVariable
import io.github.wulkanowy.utils.toFormattedString
import kotlinx.android.synthetic.main.dialog_exam.*
class ExamDialog : DialogFragment() {
private var binding: DialogExamBinding by lifecycleAwareVariable()
private lateinit var exam: Exam
companion object {
@ -33,18 +35,20 @@ class ExamDialog : DialogFragment() {
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.dialog_exam, container, false)
return DialogExamBinding.inflate(inflater).apply { binding = this }.root
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
examDialogSubjectValue.text = exam.subject
examDialogTypeValue.text = exam.type
examDialogTeacherValue.text = exam.teacher
examDialogDateValue.text = exam.entryDate.toFormattedString()
examDialogDescriptionValue.text = exam.description
with(binding) {
examDialogSubjectValue.text = exam.subject
examDialogTypeValue.text = exam.type
examDialogTeacherValue.text = exam.teacher
examDialogDateValue.text = exam.entryDate.toFormattedString()
examDialogDescriptionValue.text = exam.description
examDialogClose.setOnClickListener { dismiss() }
examDialogClose.setOnClickListener { dismiss() }
}
}
}

View File

@ -1,33 +1,29 @@
package io.github.wulkanowy.ui.modules.exam
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.View.GONE
import android.view.View.INVISIBLE
import android.view.View.VISIBLE
import android.view.ViewGroup
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.common.FlexibleItemDecoration
import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import androidx.recyclerview.widget.LinearLayoutManager
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Exam
import io.github.wulkanowy.databinding.FragmentExamBinding
import io.github.wulkanowy.ui.base.BaseFragment
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.setOnItemClickListener
import kotlinx.android.synthetic.main.fragment_exam.*
import javax.inject.Inject
class ExamFragment : BaseFragment(), ExamView, MainView.MainChildView, MainView.TitledView {
class ExamFragment : BaseFragment<FragmentExamBinding>(R.layout.fragment_exam), ExamView,
MainView.MainChildView, MainView.TitledView {
@Inject
lateinit var presenter: ExamPresenter
@Inject
lateinit var examAdapter: FlexibleAdapter<AbstractFlexibleItem<*>>
lateinit var examAdapter: ExamAdapter
companion object {
private const val SAVED_DATE_KEY = "CURRENT_DATE"
@ -37,57 +33,60 @@ class ExamFragment : BaseFragment(), ExamView, MainView.MainChildView, MainView.
override val titleStringId get() = R.string.exam_title
override val isViewEmpty get() = examAdapter.isEmpty
override val isViewEmpty get() = examAdapter.items.isEmpty()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_exam, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
messageContainer = examRecycler
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentExamBinding.bind(view)
messageContainer = binding.examRecycler
presenter.onAttachView(this, savedInstanceState?.getLong(SAVED_DATE_KEY))
}
override fun initView() {
examAdapter.setOnItemClickListener(presenter::onExamItemSelected)
examAdapter.onClickListener = presenter::onExamItemSelected
with(examRecycler) {
layoutManager = SmoothScrollLinearLayoutManager(context)
with(binding.examRecycler) {
layoutManager = LinearLayoutManager(context)
adapter = examAdapter
addItemDecoration(FlexibleItemDecoration(context)
.withDefaultDivider(R.layout.item_exam)
.withDrawDividerOnLastItem(false))
addItemDecoration(DividerItemDecoration(context))
}
examSwipe.setOnRefreshListener(presenter::onSwipeRefresh)
examErrorRetry.setOnClickListener { presenter.onRetry() }
examErrorDetails.setOnClickListener { presenter.onDetailsClick() }
with(binding) {
examSwipe.setOnRefreshListener(presenter::onSwipeRefresh)
examErrorRetry.setOnClickListener { presenter.onRetry() }
examErrorDetails.setOnClickListener { presenter.onDetailsClick() }
examPreviousButton.setOnClickListener { presenter.onPreviousWeek() }
examNextButton.setOnClickListener { presenter.onNextWeek() }
examPreviousButton.setOnClickListener { presenter.onPreviousWeek() }
examNextButton.setOnClickListener { presenter.onNextWeek() }
examNavContainer.setElevationCompat(requireContext().dpToPx(8f))
examNavContainer.setElevationCompat(requireContext().dpToPx(8f))
}
}
override fun hideRefresh() {
examSwipe.isRefreshing = false
binding.examSwipe.isRefreshing = false
}
override fun updateData(data: List<ExamItem>) {
examAdapter.updateDataSet(data, true)
override fun updateData(data: List<ExamItem<*>>) {
with(examAdapter) {
items = data
notifyDataSetChanged()
}
}
override fun updateNavigationWeek(date: String) {
examNavDate.text = date
binding.examNavDate.text = date
}
override fun clearData() {
examAdapter.clear()
with(examAdapter) {
items = emptyList()
notifyDataSetChanged()
}
}
override fun resetView() {
examRecycler.scrollToPosition(0)
binding.examRecycler.scrollToPosition(0)
}
override fun onFragmentReselected() {
@ -95,35 +94,35 @@ class ExamFragment : BaseFragment(), ExamView, MainView.MainChildView, MainView.
}
override fun showEmpty(show: Boolean) {
examEmpty.visibility = if (show) VISIBLE else GONE
binding.examEmpty.visibility = if (show) VISIBLE else GONE
}
override fun showErrorView(show: Boolean) {
examError.visibility = if (show) VISIBLE else GONE
binding.examError.visibility = if (show) VISIBLE else GONE
}
override fun setErrorDetails(message: String) {
examErrorMessage.text = message
binding.examErrorMessage.text = message
}
override fun showProgress(show: Boolean) {
examProgress.visibility = if (show) VISIBLE else GONE
binding.examProgress.visibility = if (show) VISIBLE else GONE
}
override fun enableSwipe(enable: Boolean) {
examSwipe.isEnabled = enable
binding.examSwipe.isEnabled = enable
}
override fun showContent(show: Boolean) {
examRecycler.visibility = if (show) VISIBLE else GONE
binding.examRecycler.visibility = if (show) VISIBLE else GONE
}
override fun showPreButton(show: Boolean) {
examPreviousButton.visibility = if (show) VISIBLE else INVISIBLE
binding.examPreviousButton.visibility = if (show) VISIBLE else INVISIBLE
}
override fun showNextButton(show: Boolean) {
examNextButton.visibility = if (show) VISIBLE else INVISIBLE
binding.examNextButton.visibility = if (show) VISIBLE else INVISIBLE
}
override fun showExamDialog(exam: Exam) {

View File

@ -1,52 +0,0 @@
package io.github.wulkanowy.ui.modules.exam
import android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractHeaderItem
import eu.davidea.flexibleadapter.items.IFlexible
import eu.davidea.viewholders.ExpandableViewHolder
import io.github.wulkanowy.R
import io.github.wulkanowy.utils.toFormattedString
import io.github.wulkanowy.utils.weekDayName
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.header_exam.*
import org.threeten.bp.LocalDate
class ExamHeader(private val date: LocalDate) : AbstractHeaderItem<ExamHeader.ViewHolder>() {
override fun getLayoutRes() = R.layout.header_exam
override fun createViewHolder(view: View?, adapter: FlexibleAdapter<IFlexible<*>>?): ViewHolder {
return ViewHolder(view, adapter)
}
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<*>>?, holder: ViewHolder,
position: Int, payloads: MutableList<Any>?) {
holder.run {
examHeaderDay.text = date.weekDayName.capitalize()
examHeaderDate.text = date.toFormattedString()
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as ExamHeader
if (date != other.date) return false
return true
}
override fun hashCode(): Int {
return date.hashCode()
}
class ViewHolder(view: View?, adapter: FlexibleAdapter<IFlexible<*>>?) : ExpandableViewHolder(view, adapter),
LayoutContainer {
override val containerView: View
get() = contentView
}
}

View File

@ -1,50 +1,9 @@
package io.github.wulkanowy.ui.modules.exam
import android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractSectionableItem
import eu.davidea.flexibleadapter.items.IFlexible
import eu.davidea.viewholders.FlexibleViewHolder
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Exam
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.item_exam.*
data class ExamItem<out T>(val value: T, val viewType: ViewType) {
class ExamItem(header: ExamHeader, val exam: Exam) : AbstractSectionableItem<ExamItem.ViewHolder, ExamHeader>(header) {
override fun getLayoutRes() = R.layout.item_exam
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<*>>): ViewHolder {
return ViewHolder(view, adapter)
}
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<*>>, holder: ViewHolder, position: Int, payloads: MutableList<Any>?) {
holder.run {
examItemSubject.text = exam.subject
examItemTeacher.text = exam.teacher
examItemType.text = exam.type
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as ExamItem
if (exam != other.exam) return false
return true
}
override fun hashCode(): Int {
var result = exam.hashCode()
result = 31 * result + exam.id.toInt()
return result
}
class ViewHolder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter), LayoutContainer {
override val containerView: View
get() = contentView
enum class ViewType(val id: Int) {
HEADER(1),
ITEM(2)
}
}

View File

@ -1,6 +1,5 @@
package io.github.wulkanowy.ui.modules.exam
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import io.github.wulkanowy.data.db.entities.Exam
import io.github.wulkanowy.data.repositories.exam.ExamRepository
import io.github.wulkanowy.data.repositories.semester.SemesterRepository
@ -9,7 +8,7 @@ import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.utils.FirebaseAnalyticsHelper
import io.github.wulkanowy.utils.SchedulersProvider
import io.github.wulkanowy.utils.friday
import io.github.wulkanowy.utils.sunday
import io.github.wulkanowy.utils.getLastSchoolDayIfHoliday
import io.github.wulkanowy.utils.isHolidays
import io.github.wulkanowy.utils.monday
@ -19,7 +18,6 @@ import org.threeten.bp.LocalDate
import org.threeten.bp.LocalDate.now
import org.threeten.bp.LocalDate.ofEpochDay
import timber.log.Timber
import java.util.concurrent.TimeUnit.MILLISECONDS
import javax.inject.Inject
class ExamPresenter @Inject constructor(
@ -75,11 +73,9 @@ class ExamPresenter @Inject constructor(
view?.showErrorDetailsDialog(lastError)
}
fun onExamItemSelected(item: AbstractFlexibleItem<*>?) {
if (item is ExamItem) {
Timber.i("Select exam item ${item.exam.id}")
view?.showExamDialog(item.exam)
}
fun onExamItemSelected(exam: Exam) {
Timber.i("Select exam item ${exam.id}")
view?.showExamDialog(exam)
}
fun onViewReselected() {
@ -114,11 +110,9 @@ class ExamPresenter @Inject constructor(
add(studentRepository.getCurrentStudent()
.flatMap { student ->
semesterRepository.getCurrentSemester(student).flatMap { semester ->
examRepository.getExams(student, semester, currentDate.monday, currentDate.friday, forceRefresh)
examRepository.getExams(student, semester, currentDate.monday, currentDate.sunday, forceRefresh)
}
}
.delay(200, MILLISECONDS)
.map { it.groupBy { exam -> exam.date }.toSortedMap() }
.map { createExamItems(it) }
.subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread)
@ -137,7 +131,12 @@ class ExamPresenter @Inject constructor(
showErrorView(false)
showContent(it.isNotEmpty())
}
analytics.logEvent("load_exam", "items" to it.size, "force_refresh" to forceRefresh)
analytics.logEvent(
"load_data",
"type" to "exam",
"items" to it.size,
"force_refresh" to forceRefresh
)
}) {
Timber.i("Loading exam result: An exception occurred")
errorHandler.dispatch(it)
@ -156,12 +155,12 @@ class ExamPresenter @Inject constructor(
}
}
private fun createExamItems(items: Map<LocalDate, List<Exam>>): List<ExamItem> {
return items.flatMap {
ExamHeader(it.key).let { header ->
it.value.reversed().map { item -> ExamItem(header, item) }
private fun createExamItems(items: List<Exam>): List<ExamItem<*>> {
return items.groupBy { it.date }.toSortedMap().map { (date, exams) ->
listOf(ExamItem(date, ExamItem.ViewType.HEADER)) + exams.reversed().map { exam ->
ExamItem(exam, ExamItem.ViewType.ITEM)
}
}
}.flatten()
}
private fun reloadView() {
@ -182,7 +181,7 @@ class ExamPresenter @Inject constructor(
showPreButton(!currentDate.minusDays(7).isHolidays)
showNextButton(!currentDate.plusDays(7).isHolidays)
updateNavigationWeek("${currentDate.monday.toFormattedString("dd.MM")} - " +
currentDate.friday.toFormattedString("dd.MM"))
currentDate.sunday.toFormattedString("dd.MM"))
}
}
}

View File

@ -9,7 +9,7 @@ interface ExamView : BaseView {
fun initView()
fun updateData(data: List<ExamItem>)
fun updateData(data: List<ExamItem<*>>)
fun updateNavigationWeek(date: String)

View File

@ -0,0 +1,11 @@
package io.github.wulkanowy.ui.modules.grade
enum class GradeAverageMode(val value: String) {
ALL_YEAR("all_year"),
ONE_SEMESTER("only_one_semester"),
BOTH_SEMESTERS("both_semesters");
companion object {
fun getByValue(value: String) = values().firstOrNull { it.value == value } ?: ONE_SEMESTER
}
}

View File

@ -1,73 +1,125 @@
package io.github.wulkanowy.ui.modules.grade
import io.github.wulkanowy.data.db.entities.Grade
import io.github.wulkanowy.data.db.entities.GradeSummary
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.repositories.grade.GradeRepository
import io.github.wulkanowy.data.repositories.gradessummary.GradeSummaryRepository
import io.github.wulkanowy.data.repositories.preferences.PreferencesRepository
import io.github.wulkanowy.data.repositories.semester.SemesterRepository
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.ui.modules.grade.GradeAverageMode.ALL_YEAR
import io.github.wulkanowy.ui.modules.grade.GradeAverageMode.BOTH_SEMESTERS
import io.github.wulkanowy.ui.modules.grade.GradeAverageMode.ONE_SEMESTER
import io.github.wulkanowy.utils.calcAverage
import io.github.wulkanowy.utils.changeModifier
import io.reactivex.Maybe
import io.reactivex.Single
import javax.inject.Inject
class GradeAverageProvider @Inject constructor(
private val preferencesRepository: PreferencesRepository,
private val semesterRepository: SemesterRepository,
private val gradeRepository: GradeRepository,
private val gradeSummaryRepository: GradeSummaryRepository
private val preferencesRepository: PreferencesRepository
) {
private val plusModifier = preferencesRepository.gradePlusModifier
private val plusModifier get() = preferencesRepository.gradePlusModifier
private val minusModifier = preferencesRepository.gradeMinusModifier
private val minusModifier get() = preferencesRepository.gradeMinusModifier
fun getGradeAverage(student: Student, semesters: List<Semester>, selectedSemesterId: Int, forceRefresh: Boolean): Single<List<Triple<String, Double, String>>> {
return when (preferencesRepository.gradeAverageMode) {
"all_year" -> getAllYearAverage(student, semesters, selectedSemesterId, forceRefresh)
"only_one_semester" -> getOnlyOneSemesterAverage(student, semesters, selectedSemesterId, forceRefresh)
else -> throw IllegalArgumentException("Incorrect grade average mode: ${preferencesRepository.gradeAverageMode} ")
fun getGradesDetailsWithAverage(student: Student, semesterId: Int, forceRefresh: Boolean = false): Single<List<GradeDetailsWithAverage>> {
return semesterRepository.getSemesters(student).flatMap { semesters ->
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)
}
}
}
private fun getAllYearAverage(student: Student, semesters: List<Semester>, semesterId: Int, forceRefresh: Boolean): Single<List<Triple<String, Double, String>>> {
private fun calculateBothSemestersAverage(student: Student, semesters: List<Semester>, semesterId: Int, forceRefresh: Boolean): Single<List<GradeDetailsWithAverage>> {
val selectedSemester = semesters.single { it.semesterId == semesterId }
val firstSemester = semesters.single { it.diaryId == selectedSemester.diaryId && it.semesterName == 1 }
return getAverageFromGradeSummary(student, selectedSemester, forceRefresh)
.switchIfEmpty(gradeRepository.getGrades(student, selectedSemester, forceRefresh)
.flatMap { firstGrades ->
if (selectedSemester == firstSemester) Single.just(firstGrades)
else {
gradeRepository.getGrades(student, firstSemester)
.map { secondGrades -> secondGrades + firstGrades }
return getSemesterDetailsWithAverage(student, selectedSemester, forceRefresh).flatMap { selectedDetails ->
val isAnyAverage = selectedDetails.any { it.average != .0 }
if (selectedSemester != firstSemester) {
getSemesterDetailsWithAverage(student, firstSemester, forceRefresh).map { secondDetails ->
selectedDetails.map { selected ->
val second = secondDetails.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)
}
}.map { grades ->
grades.map { if (student.loginMode == Sdk.Mode.SCRAPPER.name) it.changeModifier(plusModifier, minusModifier) else it }
.groupBy { it.subject }
.map { Triple(it.key, it.value.calcAverage(), "") }
})
}
} else Single.just(selectedDetails)
}
}
private fun getOnlyOneSemesterAverage(student: Student, semesters: List<Semester>, semesterId: Int, forceRefresh: Boolean): Single<List<Triple<String, Double, String>>> {
private fun calculateAllYearAverage(student: Student, semesters: List<Semester>, semesterId: Int, forceRefresh: Boolean): Single<List<GradeDetailsWithAverage>> {
val selectedSemester = semesters.single { it.semesterId == semesterId }
val firstSemester = semesters.single { it.diaryId == selectedSemester.diaryId && it.semesterName == 1 }
return getAverageFromGradeSummary(student, selectedSemester, forceRefresh)
.switchIfEmpty(gradeRepository.getGrades(student, selectedSemester, forceRefresh)
.map { grades ->
grades.map { if (student.loginMode == Sdk.Mode.SCRAPPER.name) it.changeModifier(plusModifier, minusModifier) else it }
.groupBy { it.subject }
.map { Triple(it.key, it.value.calcAverage(), "") }
})
return getSemesterDetailsWithAverage(student, selectedSemester, forceRefresh).flatMap { selectedDetails ->
val isAnyAverage = selectedDetails.any { it.average != .0 }
if (selectedSemester != firstSemester) {
getSemesterDetailsWithAverage(student, firstSemester, forceRefresh).map { secondDetails ->
selectedDetails.map { selected ->
val second = secondDetails.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)
}
}
} else Single.just(selectedDetails)
}
}
private fun getAverageFromGradeSummary(student: Student, selectedSemester: Semester, forceRefresh: Boolean): Maybe<List<Triple<String, Double, String>>> {
return gradeSummaryRepository.getGradesSummary(student, selectedSemester, forceRefresh)
.toMaybe()
.flatMap {
if (it.any { summary -> summary.average != .0 }) {
Maybe.just(it.map { summary -> Triple(summary.subject, summary.average, summary.pointsSum) })
} else Maybe.empty()
}.filter { !preferencesRepository.gradeAverageForceCalc }
private fun getSemesterDetailsWithAverage(student: Student, semester: Semester, forceRefresh: Boolean): Single<List<GradeDetailsWithAverage>> {
return gradeRepository.getGrades(student, semester, forceRefresh).map { (details, summaries) ->
val isAnyAverage = summaries.any { it.average != .0 }
val allGrades = details.groupBy { it.subject }
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
)
}
}
}
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) ->
singleOrNull { it.subject == subject }?.let { return@mapIndexed it }
GradeSummary(
studentId = student.studentId,
semesterId = semester.semesterId,
position = i,
subject = subject,
predictedGrade = "",
finalGrade = "",
proposedPoints = "",
finalPoints = "",
pointsSum = "",
average = if (calcAverage) details.updateModifiers(student).calcAverage() else .0
)
}
}
private fun List<Grade>.updateModifiers(student: Student): List<Grade> {
return if (student.loginMode == Sdk.Mode.SCRAPPER.name) {
map { it.changeModifier(plusModifier, minusModifier) }
} else this
}
}

View File

@ -0,0 +1,12 @@
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 GradeDetailsWithAverage(
val subject: String,
val average: Double,
val points: String,
val summary: GradeSummary,
val grades: List<Grade>
)

View File

@ -1,16 +1,15 @@
package io.github.wulkanowy.ui.modules.grade
import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.View.INVISIBLE
import android.view.View.VISIBLE
import android.view.ViewGroup
import androidx.appcompat.app.AlertDialog
import io.github.wulkanowy.R
import io.github.wulkanowy.databinding.FragmentGradeBinding
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.base.BaseFragmentPagerAdapter
import io.github.wulkanowy.ui.modules.grade.details.GradeDetailsFragment
@ -19,10 +18,9 @@ import io.github.wulkanowy.ui.modules.grade.summary.GradeSummaryFragment
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.dpToPx
import io.github.wulkanowy.utils.setOnSelectPageListener
import kotlinx.android.synthetic.main.fragment_grade.*
import javax.inject.Inject
class GradeFragment : BaseFragment(), GradeView, MainView.MainChildView, MainView.TitledView {
class GradeFragment : BaseFragment<FragmentGradeBinding>(R.layout.fragment_grade), GradeView, MainView.MainChildView, MainView.TitledView {
@Inject
lateinit var presenter: GradePresenter
@ -42,19 +40,16 @@ class GradeFragment : BaseFragment(), GradeView, MainView.MainChildView, MainVie
override var subtitleString = ""
override val currentPageIndex get() = gradeViewPager.currentItem
override val currentPageIndex get() = binding.gradeViewPager.currentItem
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_grade, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentGradeBinding.bind(view)
presenter.onAttachView(this, savedInstanceState?.getInt(SAVED_SEMESTER_KEY))
}
@ -66,7 +61,7 @@ class GradeFragment : BaseFragment(), GradeView, MainView.MainChildView, MainVie
override fun initView() {
with(pagerAdapter) {
containerId = gradeViewPager.id
containerId = binding.gradeViewPager.id
addFragmentsWithTitle(mapOf(
GradeDetailsFragment.newInstance() to getString(R.string.all_details),
GradeSummaryFragment.newInstance() to getString(R.string.grade_menu_summary),
@ -74,19 +69,21 @@ class GradeFragment : BaseFragment(), GradeView, MainView.MainChildView, MainVie
))
}
with(gradeViewPager) {
with(binding.gradeViewPager) {
adapter = pagerAdapter
offscreenPageLimit = 3
setOnSelectPageListener(presenter::onPageSelected)
}
with(gradeTabLayout) {
setupWithViewPager(gradeViewPager)
with(binding.gradeTabLayout) {
setupWithViewPager(binding.gradeViewPager)
setElevationCompat(context.dpToPx(4f))
}
gradeErrorRetry.setOnClickListener { presenter.onRetry() }
gradeErrorDetails.setOnClickListener { presenter.onDetailsClick() }
with(binding) {
gradeErrorRetry.setOnClickListener { presenter.onRetry() }
gradeErrorDetails.setOnClickListener { presenter.onDetailsClick() }
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
@ -99,20 +96,22 @@ class GradeFragment : BaseFragment(), GradeView, MainView.MainChildView, MainVie
}
override fun showContent(show: Boolean) {
gradeViewPager.visibility = if (show) VISIBLE else INVISIBLE
gradeTabLayout.visibility = if (show) VISIBLE else INVISIBLE
with(binding) {
gradeViewPager.visibility = if (show) VISIBLE else INVISIBLE
gradeTabLayout.visibility = if (show) VISIBLE else INVISIBLE
}
}
override fun showProgress(show: Boolean) {
gradeProgress.visibility = if (show) VISIBLE else INVISIBLE
binding.gradeProgress.visibility = if (show) VISIBLE else INVISIBLE
}
override fun showErrorView(show: Boolean) {
gradeError.visibility = if (show) VISIBLE else INVISIBLE
binding.gradeError.visibility = if (show) VISIBLE else INVISIBLE
}
override fun setErrorDetails(message: String) {
gradeErrorMessage.text = message
binding.gradeErrorMessage.text = message
}
override fun showSemesterSwitch(show: Boolean) {
@ -166,7 +165,7 @@ class GradeFragment : BaseFragment(), GradeView, MainView.MainChildView, MainVie
}
override fun onDestroyView() {
super.onDestroyView()
presenter.onDetachView()
super.onDestroyView()
}
}

View File

@ -133,6 +133,7 @@ class GradePresenter @Inject constructor(
}
private fun loadChild(index: Int, forceRefresh: Boolean = false) {
Timber.d("Load grade tab child. Selected semester: $selectedIndex, semesters: ${semesters.joinToString { it.semesterName.toString() }}")
semesters.first { it.semesterName == selectedIndex }.semesterId.also {
if (forceRefresh || loadedSemesterId[index] != it) {
Timber.i("Load grade child view index: $index")

View File

@ -0,0 +1,198 @@
package io.github.wulkanowy.ui.modules.grade.details
import android.annotation.SuppressLint
import android.content.res.Resources
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.NO_POSITION
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Grade
import io.github.wulkanowy.databinding.HeaderGradeDetailsBinding
import io.github.wulkanowy.databinding.ItemGradeDetailsBinding
import io.github.wulkanowy.ui.base.BaseExpandableAdapter
import io.github.wulkanowy.utils.getBackgroundColor
import io.github.wulkanowy.utils.toFormattedString
import timber.log.Timber
import javax.inject.Inject
class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter<RecyclerView.ViewHolder>() {
private var headers = mutableListOf<GradeDetailsItem>()
private var items = mutableListOf<GradeDetailsItem>()
private var expandedPosition = NO_POSITION
private var isExpandable = false
var onClickListener: (Grade, position: Int) -> Unit = { _, _ -> }
var colorTheme = ""
fun setDataItems(data: List<GradeDetailsItem>, isExpanded: Boolean = isExpandable) {
headers = data.filter { it.viewType == ViewType.HEADER }.toMutableList()
items = if (isExpanded) headers else data.toMutableList()
isExpandable = isExpanded
expandedPosition = NO_POSITION
}
fun updateDetailsItem(position: Int, grade: Grade) {
if (items.getOrNull(position)?.viewType != ViewType.ITEM) {
Timber.e("Trying to update item $position on list ${items.size} size, expanded position: $expandedPosition")
return
}
items[position] = GradeDetailsItem(grade, ViewType.ITEM)
notifyItemChanged(position)
}
fun getHeaderItem(subject: String): GradeDetailsItem {
val candidates = headers.filter { (it.value as GradeDetailsHeader).subject == subject }
if (candidates.size > 1) {
Timber.e("Header with subject $subject found ${candidates.size} times! Expanded: $expandedPosition. Items: $candidates")
}
return candidates.first()
}
fun updateHeaderItem(item: GradeDetailsItem) {
val headerPosition = headers.indexOf(item)
val itemPosition = items.indexOf(item)
if (headerPosition == NO_POSITION || itemPosition == NO_POSITION) {
Timber.e("Invalid update header positions! Header: $headerPosition, item: $itemPosition")
}
headers[headerPosition] = item
items[itemPosition] = item
notifyItemChanged(itemPosition)
}
fun collapseAll() {
if (expandedPosition != -1) {
refreshList(headers)
expandedPosition = NO_POSITION
}
}
@Synchronized
private fun refreshList(newItems: MutableList<GradeDetailsItem>) {
val diffCallback = GradeDetailsDiffUtil(items, newItems)
val diffResult = DiffUtil.calculateDiff(diffCallback)
items = newItems
diffResult.dispatchUpdatesTo(this)
}
override fun getItemCount() = items.size
override fun getItemViewType(position: Int) = items[position].viewType.id
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context)
return when (viewType) {
ViewType.HEADER.id -> HeaderViewHolder(HeaderGradeDetailsBinding.inflate(inflater, parent, false))
ViewType.ITEM.id -> ItemViewHolder(ItemGradeDetailsBinding.inflate(inflater, parent, false))
else -> throw IllegalStateException()
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is HeaderViewHolder -> bindHeaderViewHolder(
holder = holder,
header = items[position].value as GradeDetailsHeader,
position = position
)
is ItemViewHolder -> bindItemViewHolder(
holder = holder,
grade = items[position].value as Grade
)
}
}
private fun bindHeaderViewHolder(holder: HeaderViewHolder, header: GradeDetailsHeader, position: Int) {
val headerPosition = headers.indexOf(items[position])
val adapterPosition = holder.adapterPosition
with(holder.binding) {
gradeHeaderDivider.visibility = if (adapterPosition == 0) View.GONE else View.VISIBLE
with(gradeHeaderSubject) {
text = header.subject
maxLines = if (headerPosition == expandedPosition) 2 else 1
}
gradeHeaderAverage.text = formatAverage(header.average, root.context.resources)
gradeHeaderPointsSum.text = root.context.getString(R.string.grade_points_sum, header.pointsSum)
gradeHeaderPointsSum.visibility = if (!header.pointsSum.isNullOrEmpty()) View.VISIBLE else View.GONE
gradeHeaderNumber.text = root.context.resources.getQuantityString(R.plurals.grade_number_item, header.grades.size, header.grades.size)
gradeHeaderNote.visibility = if (header.newGrades > 0) View.VISIBLE else View.GONE
if (header.newGrades > 0) gradeHeaderNote.text = header.newGrades.toString(10)
gradeHeaderContainer.isEnabled = isExpandable
gradeHeaderContainer.setOnClickListener {
expandedPosition = if (expandedPosition == adapterPosition) -1 else adapterPosition
if (expandedPosition != NO_POSITION) {
refreshList(headers.toMutableList().apply {
addAll(headerPosition + 1, header.grades)
})
scrollToHeaderWithSubItems(headerPosition, header.grades.size)
} else {
refreshList(headers)
}
}
}
}
private fun formatAverage(average: Double?, resources: Resources): String {
return if (average == null || average == .0) resources.getString(R.string.grade_no_average)
else resources.getString(R.string.grade_average, average)
}
@SuppressLint("SetTextI18n")
private fun bindItemViewHolder(holder: ItemViewHolder, grade: Grade) {
with(holder.binding) {
gradeItemValue.run {
text = grade.entry
setBackgroundResource(grade.getBackgroundColor(colorTheme))
}
gradeItemDescription.text = when {
grade.description.isNotBlank() -> grade.description
grade.gradeSymbol.isNotBlank() -> grade.gradeSymbol
else -> root.context.getString(R.string.all_no_description)
}
gradeItemDate.text = grade.date.toFormattedString()
gradeItemWeight.text = "${root.context.getString(R.string.grade_weight)}: ${grade.weight}"
gradeItemNote.visibility = if (!grade.isRead) View.VISIBLE else View.GONE
root.setOnClickListener {
holder.adapterPosition.let { if (it != NO_POSITION) onClickListener(grade, it) }
}
}
}
private class HeaderViewHolder(val binding: HeaderGradeDetailsBinding) :
RecyclerView.ViewHolder(binding.root)
private class ItemViewHolder(val binding: ItemGradeDetailsBinding) :
RecyclerView.ViewHolder(binding.root)
class GradeDetailsDiffUtil(private val old: List<GradeDetailsItem>, private val new: List<GradeDetailsItem>) :
DiffUtil.Callback() {
override fun getOldListSize() = old.size
override fun getNewListSize() = new.size
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return old[oldItemPosition] == new[newItemPosition]
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return old[oldItemPosition] == new[newItemPosition]
}
}
}

View File

@ -8,14 +8,17 @@ import android.view.ViewGroup
import androidx.fragment.app.DialogFragment
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Grade
import io.github.wulkanowy.databinding.DialogGradeBinding
import io.github.wulkanowy.utils.colorStringId
import io.github.wulkanowy.utils.getBackgroundColor
import io.github.wulkanowy.utils.getGradeColor
import io.github.wulkanowy.utils.lifecycleAwareVariable
import io.github.wulkanowy.utils.toFormattedString
import kotlinx.android.synthetic.main.dialog_grade.*
class GradeDetailsDialog : DialogFragment() {
private var binding: DialogGradeBinding by lifecycleAwareVariable()
private lateinit var grade: Grade
private lateinit var colorScheme: String
@ -44,47 +47,49 @@ class GradeDetailsDialog : DialogFragment() {
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.dialog_grade, container, false)
return DialogGradeBinding.inflate(inflater).apply { binding = this }.root
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
gradeDialogSubject.text = grade.subject
with(binding) {
gradeDialogSubject.text = grade.subject
gradeDialogColorAndWeightValue.run {
text = context.getString(R.string.grade_weight_value, grade.weight)
setBackgroundResource(grade.getGradeColor())
}
gradeDialogDateValue.text = grade.date.toFormattedString()
gradeDialogColorValue.text = getString(grade.colorStringId)
gradeDialogCommentValue.apply {
if (grade.comment.isBlank()) {
visibility = GONE
gradeDialogComment.visibility = GONE
} else text = grade.comment
}
gradeDialogValue.run {
text = grade.entry
setBackgroundResource(grade.getBackgroundColor(colorScheme))
}
gradeDialogTeacherValue.text = if (grade.teacher.isBlank()) {
getString(R.string.all_no_data)
} else grade.teacher
gradeDialogDescriptionValue.text = grade.run {
when {
description.isBlank() && gradeSymbol.isNotBlank() -> gradeSymbol
description.isBlank() && gradeSymbol.isBlank() -> getString(R.string.all_no_description)
gradeSymbol.isNotBlank() && description.isNotBlank() -> "$gradeSymbol - $description"
else -> description
gradeDialogColorAndWeightValue.run {
text = context.getString(R.string.grade_weight_value, grade.weight)
setBackgroundResource(grade.getGradeColor())
}
}
gradeDialogClose.setOnClickListener { dismiss() }
gradeDialogDateValue.text = grade.date.toFormattedString()
gradeDialogColorValue.text = getString(grade.colorStringId)
gradeDialogCommentValue.apply {
if (grade.comment.isBlank()) {
visibility = GONE
gradeDialogComment.visibility = GONE
} else text = grade.comment
}
gradeDialogValue.run {
text = grade.entry
setBackgroundResource(grade.getBackgroundColor(colorScheme))
}
gradeDialogTeacherValue.text = if (grade.teacher.isBlank()) {
getString(R.string.all_no_data)
} else grade.teacher
gradeDialogDescriptionValue.text = grade.run {
when {
description.isBlank() && gradeSymbol.isNotBlank() -> gradeSymbol
description.isBlank() && gradeSymbol.isBlank() -> getString(R.string.all_no_description)
gradeSymbol.isNotBlank() && description.isNotBlank() -> "$gradeSymbol - $description"
else -> description
}
}
gradeDialogClose.setOnClickListener { dismiss() }
}
}
}

View File

@ -1,7 +1,6 @@
package io.github.wulkanowy.ui.modules.grade.details
import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
@ -9,29 +8,25 @@ import android.view.View
import android.view.View.GONE
import android.view.View.INVISIBLE
import android.view.View.VISIBLE
import android.view.ViewGroup
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import eu.davidea.flexibleadapter.items.IExpandable
import eu.davidea.flexibleadapter.items.IFlexible
import androidx.recyclerview.widget.LinearLayoutManager
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Grade
import io.github.wulkanowy.databinding.FragmentGradeDetailsBinding
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.grade.GradeFragment
import io.github.wulkanowy.ui.modules.grade.GradeView
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.utils.setOnItemClickListener
import kotlinx.android.synthetic.main.fragment_grade_details.*
import javax.inject.Inject
class GradeDetailsFragment : BaseFragment(), GradeDetailsView, GradeView.GradeChildView {
class GradeDetailsFragment :
BaseFragment<FragmentGradeDetailsBinding>(R.layout.fragment_grade_details), GradeDetailsView,
GradeView.GradeChildView {
@Inject
lateinit var presenter: GradeDetailsPresenter
@Inject
lateinit var gradeDetailsAdapter: FlexibleAdapter<AbstractFlexibleItem<*>>
lateinit var gradeDetailsAdapter: GradeDetailsAdapter
private var gradeDetailsMenu: Menu? = null
@ -39,36 +34,18 @@ class GradeDetailsFragment : BaseFragment(), GradeDetailsView, GradeView.GradeCh
fun newInstance() = GradeDetailsFragment()
}
override val emptyAverageString: String
get() = getString(R.string.grade_no_average)
override val averageString: String
get() = getString(R.string.grade_average)
override val pointsSumString: String
get() = getString(R.string.grade_points_sum)
override val weightString: String
get() = getString(R.string.grade_weight)
override val noDescriptionString: String
get() = getString(R.string.all_no_description)
override val isViewEmpty
get() = gradeDetailsAdapter.isEmpty
get() = gradeDetailsAdapter.itemCount == 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_grade_details, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
messageContainer = gradeDetailsRecycler
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentGradeDetailsBinding.bind(view)
messageContainer = binding.gradeDetailsRecycler
presenter.onAttachView(this)
}
@ -79,22 +56,17 @@ class GradeDetailsFragment : BaseFragment(), GradeDetailsView, GradeView.GradeCh
}
override fun initView() {
gradeDetailsAdapter.run {
isAutoCollapseOnExpand = true
isAutoScrollOnExpand = true
setOnItemClickListener { presenter.onGradeItemSelected(it) }
}
gradeDetailsAdapter.onClickListener = presenter::onGradeItemSelected
gradeDetailsRecycler.run {
layoutManager = SmoothScrollLinearLayoutManager(context)
adapter = gradeDetailsAdapter
addItemDecoration(GradeDetailsHeaderItemDecoration(context)
.withDefaultDivider(R.layout.header_grade_details)
)
with(binding) {
with(gradeDetailsRecycler) {
layoutManager = LinearLayoutManager(context)
adapter = gradeDetailsAdapter
}
gradeDetailsSwipe.setOnRefreshListener { presenter.onSwipeRefresh() }
gradeDetailsErrorRetry.setOnClickListener { presenter.onRetry() }
gradeDetailsErrorDetails.setOnClickListener { presenter.onDetailsClick() }
}
gradeDetailsSwipe.setOnRefreshListener { presenter.onSwipeRefresh() }
gradeDetailsErrorRetry.setOnClickListener { presenter.onRetry() }
gradeDetailsErrorDetails.setOnClickListener { presenter.onDetailsClick() }
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
@ -102,16 +74,23 @@ class GradeDetailsFragment : BaseFragment(), GradeDetailsView, GradeView.GradeCh
else false
}
override fun updateData(data: List<GradeDetailsHeader>) {
gradeDetailsAdapter.updateDataSet(data, true)
override fun updateData(data: List<GradeDetailsItem>, isGradeExpandable: Boolean, gradeColorTheme: String) {
with(gradeDetailsAdapter) {
colorTheme = gradeColorTheme
setDataItems(data, isGradeExpandable)
notifyDataSetChanged()
}
}
override fun updateItem(item: AbstractFlexibleItem<*>) {
gradeDetailsAdapter.updateItem(item)
override fun updateItem(item: Grade, position: Int) {
gradeDetailsAdapter.updateDetailsItem(position, item)
}
override fun clearView() {
gradeDetailsAdapter.clear()
with(gradeDetailsAdapter) {
setDataItems(mutableListOf())
notifyDataSetChanged()
}
}
override fun collapseAllItems() {
@ -119,43 +98,43 @@ class GradeDetailsFragment : BaseFragment(), GradeDetailsView, GradeView.GradeCh
}
override fun scrollToStart() {
gradeDetailsRecycler.scrollToPosition(0)
binding.gradeDetailsRecycler.smoothScrollToPosition(0)
}
override fun getHeaderOfItem(item: AbstractFlexibleItem<*>): IExpandable<*, out IFlexible<*>>? {
return gradeDetailsAdapter.getExpandableOf(item)
override fun getHeaderOfItem(subject: String): GradeDetailsItem {
return gradeDetailsAdapter.getHeaderItem(subject)
}
override fun getGradeNumberString(number: Int): String {
return resources.getQuantityString(R.plurals.grade_number_item, number, number)
override fun updateHeaderItem(item: GradeDetailsItem) {
gradeDetailsAdapter.updateHeaderItem(item)
}
override fun showProgress(show: Boolean) {
gradeDetailsProgress.visibility = if (show) VISIBLE else GONE
binding.gradeDetailsProgress.visibility = if (show) VISIBLE else GONE
}
override fun enableSwipe(enable: Boolean) {
gradeDetailsSwipe.isEnabled = enable
binding.gradeDetailsSwipe.isEnabled = enable
}
override fun showContent(show: Boolean) {
gradeDetailsRecycler.visibility = if (show) VISIBLE else INVISIBLE
binding.gradeDetailsRecycler.visibility = if (show) VISIBLE else INVISIBLE
}
override fun showEmpty(show: Boolean) {
gradeDetailsEmpty.visibility = if (show) VISIBLE else INVISIBLE
binding.gradeDetailsEmpty.visibility = if (show) VISIBLE else INVISIBLE
}
override fun showErrorView(show: Boolean) {
gradeDetailsError.visibility = if (show) VISIBLE else GONE
binding.gradeDetailsError.visibility = if (show) VISIBLE else GONE
}
override fun setErrorDetails(message: String) {
gradeDetailsErrorMessage.text = message
binding.gradeDetailsErrorMessage.text = message
}
override fun showRefresh(show: Boolean) {
gradeDetailsSwipe.isRefreshing = show
binding.gradeDetailsSwipe.isRefreshing = show
}
override fun showGradeDialog(grade: Grade, colorScheme: String) {
@ -187,7 +166,7 @@ class GradeDetailsFragment : BaseFragment(), GradeDetailsView, GradeView.GradeCh
}
override fun onDestroyView() {
super.onDestroyView()
presenter.onDetachView()
super.onDestroyView()
}
}

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