Merge branch 'release/0.18.0'

This commit is contained in:
Mikołaj Pich 2020-05-21 00:59:13 +02:00
commit 31902a7667
268 changed files with 5041 additions and 4946 deletions

View File

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

View File

@ -14,7 +14,7 @@ cache:
branches: branches:
only: only:
- develop - develop
- 0.17.4 - 0.18.0
android: android:
licenses: licenses:
@ -48,20 +48,15 @@ before_script:
script: script:
- ./gradlew dependencies --stacktrace --daemon - ./gradlew dependencies --stacktrace --daemon
- fossa --no-ansi || true - fossa --no-ansi || true
#- ./gradlew lintPlayRelease -x fabricGenerateResourcesPlayRelease --stacktrace --daemon - ./gradlew -Pcoverage testPlayDebugUnitTest --stacktrace --daemon
- ./gradlew -Pcoverage testPlayDebugUnitTest -x fabricGenerateResourcesPlay --stacktrace --daemon
- ./gradlew -Pcoverage createFdroidDebugCoverageReport --stacktrace --daemon - ./gradlew -Pcoverage createFdroidDebugCoverageReport --stacktrace --daemon
- ./gradlew -Pcoverage jacocoTestReport --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 if [ $TRAVIS_TAG ]; then
gpg --yes --batch --passphrase=$SERVICES_ENCRYPT_KEY ./app/src/release/google-services.json.gpg; 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/key.p12.gpg;
gpg --yes --batch --passphrase=$ENCRYPT_KEY ./app/upload-key.jks.gpg; gpg --yes --batch --passphrase=$ENCRYPT_KEY ./app/upload-key.jks.gpg;
./gradlew publishPlayRelease -PenableCrashlytics --stacktrace; ./gradlew publishPlayRelease -PenableFirebase --stacktrace;
fi fi
after_success: after_success:

View File

@ -1,8 +1,7 @@
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt' apply plugin: 'kotlin-kapt'
apply plugin: 'kotlin-android-extensions' apply plugin: 'com.google.firebase.crashlytics'
apply plugin: 'io.fabric'
apply plugin: 'com.github.triplet.play' apply plugin: 'com.github.triplet.play'
apply plugin: 'com.mikepenz.aboutlibraries.plugin' apply plugin: 'com.mikepenz.aboutlibraries.plugin'
apply from: 'jacoco.gradle' apply from: 'jacoco.gradle'
@ -18,14 +17,13 @@ android {
testApplicationId "io.github.tests.wulkanowy" testApplicationId "io.github.tests.wulkanowy"
minSdkVersion 17 minSdkVersion 17
targetSdkVersion 29 targetSdkVersion 29
versionCode 57 versionCode 59
versionName "0.17.4" versionName "0.18.0"
multiDexEnabled true multiDexEnabled true
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true
manifestPlaceholders = [ manifestPlaceholders = [
fabric_api_key : System.getenv("FABRIC_API_KEY") ?: "null", firebase_enabled: project.hasProperty("enableFirebase")
crashlytics_enabled: project.hasProperty("enableCrashlytics")
] ]
javaCompileOptions { javaCompileOptions {
annotationProcessorOptions { annotationProcessorOptions {
@ -52,18 +50,16 @@ android {
buildTypes { buildTypes {
release { release {
buildConfigField "boolean", "CRASHLYTICS_ENABLED", "true"
minifyEnabled true minifyEnabled true
shrinkResources true shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release signingConfig signingConfigs.release
} }
debug { debug {
buildConfigField "boolean", "CRASHLYTICS_ENABLED", project.hasProperty("enableCrashlytics") ? "true" : "false"
applicationIdSuffix ".dev" applicationIdSuffix ".dev"
versionNameSuffix "-dev" versionNameSuffix "-dev"
testCoverageEnabled = project.hasProperty('coverage') testCoverageEnabled = project.hasProperty('coverage')
ext.enableCrashlytics = project.hasProperty("enableCrashlytics") ext.enableCrashlytics = project.hasProperty("enableFirebase")
} }
} }
@ -75,11 +71,14 @@ android {
} }
fdroid { fdroid {
buildConfigField "boolean", "CRASHLYTICS_ENABLED", "false"
dimension "platform" dimension "platform"
} }
} }
viewBinding {
enabled = true
}
lintOptions { lintOptions {
disable 'HardwareIds' disable 'HardwareIds'
} }
@ -103,10 +102,6 @@ android {
} }
} }
androidExtensions {
experimental = true
}
play { play {
serviceAccountEmail = System.getenv("PLAY_SERVICE_ACCOUNT_EMAIL") ?: "jan@fakelog.cf" serviceAccountEmail = System.getenv("PLAY_SERVICE_ACCOUNT_EMAIL") ?: "jan@fakelog.cf"
serviceAccountCredentials = file('key.p12') serviceAccountCredentials = file('key.p12')
@ -118,7 +113,6 @@ ext {
work_manager = "2.3.4" work_manager = "2.3.4"
room = "2.2.5" room = "2.2.5"
dagger = "2.27" dagger = "2.27"
// don't update https://github.com/ChuckerTeam/chucker/issues/242
chucker = "3.2.0" chucker = "3.2.0"
mockk = "1.9.2" mockk = "1.9.2"
} }
@ -128,12 +122,12 @@ configurations.all {
} }
dependencies { dependencies {
implementation "io.github.wulkanowy:sdk:0.17.4" implementation "io.github.wulkanowy:sdk:0.18.0"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation "androidx.core:core-ktx:1.2.0" implementation "androidx.core:core-ktx:1.2.0"
implementation "androidx.activity:activity-ktx:1.1.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.appcompat:appcompat-resources:1.1.0"
implementation "androidx.fragment:fragment-ktx:1.2.4" implementation "androidx.fragment:fragment-ktx:1.2.4"
implementation "androidx.annotation:annotation:1.1.0" implementation "androidx.annotation:annotation:1.1.0"
@ -167,30 +161,29 @@ dependencies {
implementation "com.squareup.inject:assisted-inject-annotations-dagger2:0.5.2" implementation "com.squareup.inject:assisted-inject-annotations-dagger2:0.5.2"
kapt "com.squareup.inject:assisted-inject-processor-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.aurelhubert:ahbottomnavigation:2.3.4"
implementation "com.ncapdevi:frag-nav:3.3.0" 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:rxandroid:2.1.1"
implementation "io.reactivex.rxjava2:rxjava:2.2.19" implementation "io.reactivex.rxjava2:rxjava:2.2.19"
implementation "com.google.code.gson:gson:2.8.6" 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 "com.jakewharton.timber:timber:4.7.1"
implementation "at.favre.lib:slf4j-timber:1.0.1" implementation "at.favre.lib:slf4j-timber:1.0.1"
implementation "fr.bipi.treessence:treessence:0.3.2" implementation "fr.bipi.treessence:treessence:0.3.2"
implementation "com.mikepenz:aboutlibraries-core:$about_libraries" implementation "com.mikepenz:aboutlibraries-core:$about_libraries"
implementation 'com.wdullaer:materialdatetimepicker:4.2.3' 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-analytics:17.4.1'
playImplementation 'com.google.firebase:firebase-inappmessaging-display-ktx:19.0.5' playImplementation 'com.google.firebase:firebase-inappmessaging-display-ktx:19.0.6'
playImplementation "com.google.firebase:firebase-inappmessaging-ktx:19.0.5" playImplementation "com.google.firebase:firebase-inappmessaging-ktx:19.0.6"
playImplementation "com.google.firebase:firebase-messaging:20.1.0" playImplementation 'com.google.firebase:firebase-messaging:20.1.7'
playImplementation "com.crashlytics.sdk.android:crashlytics:2.10.1" playImplementation 'com.google.firebase:firebase-crashlytics:17.0.0'
playImplementation 'com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava' playImplementation 'com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava'
releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker" releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker"
@ -200,7 +193,7 @@ dependencies {
testImplementation "junit:junit:4.13" testImplementation "junit:junit:4.13"
testImplementation "io.mockk:mockk:$mockk" 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" testImplementation "org.mockito:mockito-inline:3.3.3"
androidTestImplementation "androidx.test:core:1.2.0" androidTestImplementation "androidx.test:core:1.2.0"

View File

@ -24,7 +24,7 @@ class GradeLocalTest {
fun createDb() { fun createDb() {
testDb = Room.inMemoryDatabaseBuilder(ApplicationProvider.getApplicationContext(), AppDatabase::class.java) testDb = Room.inMemoryDatabaseBuilder(ApplicationProvider.getApplicationContext(), AppDatabase::class.java)
.build() .build()
gradeLocal = GradeLocal(testDb.gradeDao) gradeLocal = GradeLocal(testDb.gradeDao, testDb.gradeSummaryDao)
} }
@After @After
@ -43,7 +43,7 @@ class GradeLocalTest {
val semester = Semester(1, 2, "", 2019, 2, 1, now(), now(), 1, 1) val semester = Semester(1, 2, "", 2019, 2, 1, now(), now(), 1, 1)
val grades = gradeLocal val grades = gradeLocal
.getGrades(semester) .getGradesDetails(semester)
.blockingGet() .blockingGet()
assertEquals(2, grades.size) 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.db.entities.Student
import io.github.wulkanowy.data.repositories.TestInternetObservingStrategy import io.github.wulkanowy.data.repositories.TestInternetObservingStrategy
import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.sdk.pojo.Grade
import io.mockk.MockKAnnotations import io.mockk.MockKAnnotations
import io.mockk.every import io.mockk.every
import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.MockK
@ -52,7 +53,7 @@ class GradeRepositoryTest {
fun initApi() { fun initApi() {
MockKAnnotations.init(this) MockKAnnotations.init(this)
testDb = Room.inMemoryDatabaseBuilder(getApplicationContext(), AppDatabase::class.java).build() testDb = Room.inMemoryDatabaseBuilder(getApplicationContext(), AppDatabase::class.java).build()
gradeLocal = GradeLocal(testDb.gradeDao) gradeLocal = GradeLocal(testDb.gradeDao, testDb.gradeSummaryDao)
gradeRemote = GradeRemote(mockSdk) gradeRemote = GradeRemote(mockSdk)
every { studentMock.registrationDate } returns LocalDateTime.of(2019, 2, 27, 12, 0) 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, 26), "przed zalogowanie w aplikacji"),
createGradeApi(5, 4.0, of(2019, 2, 27), "Ocena z dnia logowania"), createGradeApi(5, 4.0, of(2019, 2, 27), "Ocena z dnia logowania"),
createGradeApi(5, 4.0, of(2019, 2, 28), "Ocena jeszcze nowsza") createGradeApi(5, 4.0, of(2019, 2, 28), "Ocena jeszcze nowsza")
)) ) to emptyList())
val grades = GradeRepository(settings, gradeLocal, gradeRemote) 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[0].isRead }
assertFalse { grades[1].isRead } assertFalse { grades[1].isRead }
@ -99,10 +100,10 @@ class GradeRepositoryTest {
createGradeApi(4, 3.0, of(2019, 2, 26), "starszą niż ostatnia lokalnie"), 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(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") createGradeApi(2, 5.0, of(2019, 2, 28), "Ta jest już w ogóle nowa")
)) ) to emptyList())
val grades = GradeRepository(settings, gradeLocal, gradeRemote) 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[0].isRead }
assertFalse { grades[1].isRead } assertFalse { grades[1].isRead }
@ -121,12 +122,12 @@ class GradeRepositoryTest {
every { mockSdk.getGrades(1) } returns Single.just(listOf( every { mockSdk.getGrades(1) } returns Single.just(listOf(
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") createGradeApi(3, 5.0, of(2019, 2, 26), "Jakaś inna ocena")
)) ) to emptyList())
val grades = GradeRepository(settings, gradeLocal, gradeRemote) val grades = GradeRepository(settings, gradeLocal, gradeRemote)
.getGrades(studentMock, semesterMock, true).blockingGet() .getGrades(studentMock, semesterMock, true).blockingGet()
assertEquals(2, grades.size) assertEquals(2, grades.first.size)
} }
@Test @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(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") createGradeApi(3, 5.0, of(2019, 2, 26), "Jakaś inna ocena")
)) ) to emptyList())
val grades = GradeRepository(settings, gradeLocal, gradeRemote) val grades = GradeRepository(settings, gradeLocal, gradeRemote)
.getGrades(studentMock, semesterMock, true).blockingGet() .getGrades(studentMock, semesterMock, true).blockingGet()
assertEquals(3, grades.size) assertEquals(3, grades.first.size)
} }
@Test @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(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") createGradeApi(3, 5.0, of(2019, 2, 26), "Jakaś inna ocena")
)) ) to emptyList())
val grades = GradeRepository(settings, gradeLocal, gradeRemote) val grades = GradeRepository(settings, gradeLocal, gradeRemote)
.getGrades(studentMock, semesterMock, true).blockingGet() .getGrades(studentMock, semesterMock, true).blockingGet()
assertEquals(3, grades.size) assertEquals(3, grades.first.size)
} }
@Test @Test
@ -171,11 +172,11 @@ class GradeRepositoryTest {
createGradeLocal(3, 5.0, of(2019, 2, 26), "Jakaś inna ocena") 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) val grades = GradeRepository(settings, gradeLocal, gradeRemote)
.getGrades(studentMock, semesterMock, true).blockingGet() .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 com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.InternetObservingSettings
import io.github.wulkanowy.data.db.AppDatabase import io.github.wulkanowy.data.db.AppDatabase
import io.github.wulkanowy.data.db.entities.Semester 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.TestInternetObservingStrategy
import io.github.wulkanowy.data.repositories.getStudent import io.github.wulkanowy.data.repositories.getStudent
import io.github.wulkanowy.services.alarm.TimetableNotificationSchedulerHelper
import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.Sdk
import io.mockk.MockKAnnotations import io.mockk.MockKAnnotations
import io.mockk.every import io.mockk.every
import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.MockK
import io.mockk.mockk
import io.reactivex.Single import io.reactivex.Single
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
@ -34,11 +37,17 @@ class TimetableRepositoryTest {
.strategy(TestInternetObservingStrategy()) .strategy(TestInternetObservingStrategy())
.build() .build()
@MockK
private lateinit var studentMock: Student
private val student = getStudent() private val student = getStudent()
@MockK @MockK
private lateinit var semesterMock: Semester private lateinit var semesterMock: Semester
@MockK
private lateinit var timetableNotificationSchedulerHelper: TimetableNotificationSchedulerHelper
private lateinit var timetableRemote: TimetableRemote private lateinit var timetableRemote: TimetableRemote
private lateinit var timetableLocal: TimetableLocal private lateinit var timetableLocal: TimetableLocal
@ -52,10 +61,17 @@ class TimetableRepositoryTest {
timetableLocal = TimetableLocal(testDb.timetableDao) timetableLocal = TimetableLocal(testDb.timetableDao)
timetableRemote = TimetableRemote(mockSdk) 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.studentId } returns 1
every { semesterMock.diaryId } returns 2 every { semesterMock.diaryId } returns 2
every { semesterMock.schoolYear } returns 2019 every { semesterMock.schoolYear } returns 2019
every { semesterMock.semesterId } returns 1 every { semesterMock.semesterId } returns 1
every { mockSdk.switchDiary(any(), any()) } returns mockSdk every { mockSdk.switchDiary(any(), any()) } returns mockSdk
} }
@ -80,7 +96,7 @@ class TimetableRepositoryTest {
createTimetableRemote(of(2019, 3, 5, 10, 30), 4, "", "W-F") 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) .getTimetable(student, semesterMock, LocalDate.of(2019, 3, 5), LocalDate.of(2019, 3, 5), true)
.blockingGet() .blockingGet()
@ -126,7 +142,7 @@ class TimetableRepositoryTest {
createTimetableRemote(of(2019, 12, 25, 10, 40), 4, "126", "Matematyka", "Paweł Czwartkowski", true) 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) .getTimetable(student, semesterMock, LocalDate.of(2019, 12, 23), LocalDate.of(2019, 12, 25), true)
.blockingGet() .blockingGet()

View File

@ -18,7 +18,8 @@
android:supportsRtl="false" android:supportsRtl="false"
android:theme="@style/WulkanowyTheme" android:theme="@style/WulkanowyTheme"
android:usesCleartextTraffic="true" android:usesCleartextTraffic="true"
tools:ignore="GoogleAppIndexingWarning,UnusedAttribute"> tools:ignore="GoogleAppIndexingWarning,UnusedAttribute"
tools:replace="android:supportsRtl,android:allowBackup">
<activity <activity
android:name=".ui.modules.splash.SplashActivity" android:name=".ui.modules.splash.SplashActivity"
android:screenOrientation="portrait" android:screenOrientation="portrait"
@ -39,7 +40,8 @@
android:name=".ui.modules.main.MainActivity" android:name=".ui.modules.main.MainActivity"
android:configChanges="orientation|screenSize" android:configChanges="orientation|screenSize"
android:label="@string/main_title" android:label="@string/main_title"
android:theme="@style/WulkanowyTheme.NoActionBar" /> android:theme="@style/WulkanowyTheme.NoActionBar"
android:windowSoftInputMode="adjustPan" />
<activity <activity
android:name=".ui.modules.message.send.SendMessageActivity" android:name=".ui.modules.message.send.SendMessageActivity"
android:configChanges="orientation|screenSize" android:configChanges="orientation|screenSize"
@ -91,6 +93,8 @@
android:resource="@xml/provider_widget_lucky_number" /> android:resource="@xml/provider_widget_lucky_number" />
</receiver> </receiver>
<receiver android:name=".services.alarm.TimetableNotificationReceiver" />
<provider <provider
android:name="androidx.work.impl.WorkManagerInitializer" android:name="androidx.work.impl.WorkManagerInitializer"
android:authorities="${applicationId}.workmanager-init" android:authorities="${applicationId}.workmanager-init"
@ -107,12 +111,33 @@
android:resource="@xml/provider_paths" /> android:resource="@xml/provider_paths" />
</provider> </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 <meta-data
android:name="io.fabric.ApiKey" android:name="firebase_analytics_collection_enabled"
android:value="${fabric_api_key}" /> android:value="${firebase_enabled}" />
<meta-data
android:name="google_analytics_adid_collection_enabled"
android:value="${firebase_enabled}" />
<meta-data <meta-data
android:name="firebase_crashlytics_collection_enabled" 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 <meta-data
android:name="com.google.firebase.messaging.default_notification_icon" 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 com.yariksoffice.lingver.Lingver
import dagger.android.AndroidInjector import dagger.android.AndroidInjector
import dagger.android.support.DaggerApplication import dagger.android.support.DaggerApplication
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.utils.Log
import fr.bipi.tressence.file.FileLoggerTree import fr.bipi.tressence.file.FileLoggerTree
import io.github.wulkanowy.di.DaggerAppComponent import io.github.wulkanowy.di.DaggerAppComponent
import io.github.wulkanowy.services.sync.SyncWorkerFactory 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.CrashlyticsExceptionTree
import io.github.wulkanowy.utils.CrashlyticsTree import io.github.wulkanowy.utils.CrashlyticsTree
import io.github.wulkanowy.utils.DebugLogTree import io.github.wulkanowy.utils.DebugLogTree
import io.github.wulkanowy.utils.initCrashlytics
import io.reactivex.exceptions.UndeliverableException import io.reactivex.exceptions.UndeliverableException
import io.reactivex.plugins.RxJavaPlugins import io.reactivex.plugins.RxJavaPlugins
import timber.log.Timber import timber.log.Timber
@ -52,12 +49,10 @@ class WulkanowyApp : DaggerApplication(), Configuration.Provider {
themeManager.applyDefaultTheme() themeManager.applyDefaultTheme()
initLogging() initLogging()
initCrashlytics(this, appInfo)
} }
private fun initLogging() { private fun initLogging() {
if (appInfo.isDebug) { if (appInfo.isDebug) {
FlexibleAdapter.enableLogs(Log.Level.DEBUG)
Timber.plant(DebugLogTree()) Timber.plant(DebugLogTree())
Timber.plant(FileLoggerTree.Builder() Timber.plant(FileLoggerTree.Builder()
.withFileName("wulkanowy.%g.log") .withFileName("wulkanowy.%g.log")

View File

@ -1,9 +1,11 @@
package io.github.wulkanowy.data package io.github.wulkanowy.data
import android.app.AlarmManager
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.res.AssetManager import android.content.res.AssetManager
import android.content.res.Resources import android.content.res.Resources
import androidx.core.content.getSystemService
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.InternetObservingSettings import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.InternetObservingSettings
import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.strategy.WalledGardenInternetObservingStrategy 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 android.content.res.AssetManager
import com.google.gson.Gson import com.google.gson.Gson
import io.github.wulkanowy.data.pojos.AppCreator import io.github.wulkanowy.data.pojos.Contributor
import io.reactivex.Single import io.reactivex.Single
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@Singleton @Singleton
class AppCreatorRepository @Inject constructor(private val assets: AssetManager) { class AppCreatorRepository @Inject constructor(private val assets: AssetManager) {
fun getAppCreators(): Single<List<AppCreator>> { fun getAppCreators(): Single<List<Contributor>> {
return Single.fromCallable<List<AppCreator>> { return Single.fromCallable<List<Contributor>> {
Gson().fromJson( Gson().fromJson(
assets.open("contributors.json").bufferedReader().use { it.readText() }, assets.open("contributors.json").bufferedReader().use { it.readText() },
Array<AppCreator>::class.java Array<Contributor>::class.java
).toList() ).toList()
} }
} }

View File

@ -1,14 +1,19 @@
package io.github.wulkanowy.data.repositories.grade package io.github.wulkanowy.data.repositories.grade
import io.github.wulkanowy.data.db.dao.GradeDao 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.Grade
import io.github.wulkanowy.data.db.entities.GradeSummary
import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Semester
import io.reactivex.Maybe import io.reactivex.Maybe
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@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>) { fun saveGrades(grades: List<Grade>) {
gradeDb.insertAll(grades) gradeDb.insertAll(grades)
@ -22,7 +27,19 @@ class GradeLocal @Inject constructor(private val gradeDb: GradeDao) {
gradeDb.updateAll(grades) 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() } 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 package io.github.wulkanowy.data.repositories.grade
import io.github.wulkanowy.data.db.entities.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.Semester
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.Sdk
@ -12,11 +13,11 @@ import javax.inject.Singleton
@Singleton @Singleton
class GradeRemote @Inject constructor(private val sdk: Sdk) { 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) return sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear)
.getGrades(semester.semesterId) .getGrades(semester.semesterId)
.map { grades -> .map { (details, summary) ->
grades.map { details.map {
Grade( Grade(
studentId = semester.studentId, studentId = semester.studentId,
semesterId = semester.semesterId, semesterId = semester.semesterId,
@ -33,6 +34,19 @@ class GradeRemote @Inject constructor(private val sdk: Sdk) {
date = it.date, date = it.date,
teacher = it.teacher 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.ReactiveNetwork
import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.InternetObservingSettings 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.Grade
import io.github.wulkanowy.data.db.entities.GradeSummary
import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.utils.uniqueSubtract import io.github.wulkanowy.utils.uniqueSubtract
@ -19,34 +20,47 @@ class GradeRepository @Inject constructor(
private val remote: GradeRemote private val remote: GradeRemote
) { ) {
fun getGrades(student: Student, semester: Semester, forceRefresh: Boolean = false, notify: Boolean = false): Single<List<Grade>> { fun getGrades(student: Student, semester: Semester, forceRefresh: Boolean = false, notify: Boolean = false): Single<Pair<List<Grade>, List<GradeSummary>>> {
return local.getGrades(semester).filter { !forceRefresh } return local.getGradesDetails(semester).flatMap { details ->
.switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings) local.getGradesSummary(semester).map { summary -> details to summary }
.flatMap { }.filter { !forceRefresh }
.switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings).flatMap {
if (it) remote.getGrades(student, semester) if (it) remote.getGrades(student, semester)
else Single.error(UnknownHostException()) else Single.error(UnknownHostException())
}.flatMap { new -> }.flatMap { (newDetails, newSummary) ->
local.getGrades(semester).toSingle(emptyList()) local.getGradesDetails(semester).toSingle(emptyList())
.doOnSuccess { old -> .doOnSuccess { old ->
val notifyBreakDate = old.maxBy { it.date }?.date ?: student.registrationDate.toLocalDate() val notifyBreakDate = old.maxBy { it.date }?.date ?: student.registrationDate.toLocalDate()
local.deleteGrades(old.uniqueSubtract(new)) local.deleteGrades(old.uniqueSubtract(newDetails))
local.saveGrades(new.uniqueSubtract(old) local.saveGrades(newDetails.uniqueSubtract(old)
.onEach { .onEach {
if (it.date >= notifyBreakDate) it.apply { if (it.date >= notifyBreakDate) it.apply {
isRead = false isRead = false
if (notify) isNotified = 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.getGrades(semester).toSingle(emptyList()) }) }
}.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>> { 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>> { 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 { 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

@ -26,6 +26,9 @@ class PreferencesRepository @Inject constructor(
val isGradeExpandable: Boolean val isGradeExpandable: Boolean
get() = !getBoolean(R.string.pref_key_expand_grade, R.bool.pref_default_expand_grade) get() = !getBoolean(R.string.pref_key_expand_grade, R.bool.pref_default_expand_grade)
val showAllSubjectsOnStatisticsList: Boolean
get() = getBoolean(R.string.pref_key_grade_statistics_list, R.bool.pref_default_grade_statistics_list)
val appThemeKey = context.getString(R.string.pref_key_app_theme) val appThemeKey = context.getString(R.string.pref_key_app_theme)
val appTheme: String val appTheme: String
get() = getString(appThemeKey, R.string.pref_default_app_theme) get() = getString(appThemeKey, R.string.pref_default_app_theme)
@ -52,6 +55,10 @@ class PreferencesRepository @Inject constructor(
val isNotificationsEnable: Boolean val isNotificationsEnable: Boolean
get() = getBoolean(R.string.pref_key_notifications_enable, R.bool.pref_default_notifications_enable) get() = getBoolean(R.string.pref_key_notifications_enable, R.bool.pref_default_notifications_enable)
val isUpcomingLessonsNotificationsEnableKey = context.getString(R.string.pref_key_notifications_upcoming_lessons_enable)
val isUpcomingLessonsNotificationsEnable: Boolean
get() = getBoolean(isUpcomingLessonsNotificationsEnableKey, R.bool.pref_default_notification_upcoming_lessons_enable)
val isDebugNotificationEnableKey = context.getString(R.string.pref_key_notification_debug) val isDebugNotificationEnableKey = context.getString(R.string.pref_key_notification_debug)
val isDebugNotificationEnable: Boolean val isDebugNotificationEnable: Boolean
get() = getBoolean(isDebugNotificationEnableKey, R.bool.pref_default_notification_debug) get() = getBoolean(isDebugNotificationEnableKey, R.bool.pref_default_notification_debug)
@ -68,6 +75,9 @@ class PreferencesRepository @Inject constructor(
val showWholeClassPlan: String val showWholeClassPlan: String
get() = getString(R.string.pref_key_timetable_show_whole_class, R.string.pref_default_timetable_show_whole_class) get() = getString(R.string.pref_key_timetable_show_whole_class, R.string.pref_default_timetable_show_whole_class)
val 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: Int, default: Int) = getString(context.getString(id), default)
private fun getString(id: String, default: Int) = sharedPref.getString(id, context.getString(default)) ?: context.getString(default) private fun getString(id: String, default: Int) = sharedPref.getString(id, context.getString(default)) ?: context.getString(default)

View File

@ -5,6 +5,7 @@ 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.Semester
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.Timetable import io.github.wulkanowy.data.db.entities.Timetable
import io.github.wulkanowy.services.alarm.TimetableNotificationSchedulerHelper
import io.github.wulkanowy.utils.friday import io.github.wulkanowy.utils.friday
import io.github.wulkanowy.utils.monday import io.github.wulkanowy.utils.monday
import io.github.wulkanowy.utils.uniqueSubtract import io.github.wulkanowy.utils.uniqueSubtract
@ -18,7 +19,8 @@ import javax.inject.Singleton
class TimetableRepository @Inject constructor( class TimetableRepository @Inject constructor(
private val settings: InternetObservingSettings, private val settings: InternetObservingSettings,
private val local: TimetableLocal, 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>> { fun getTimetable(student: Student, semester: Semester, start: LocalDate, end: LocalDate, forceRefresh: Boolean = false): Single<List<Timetable>> {
@ -31,8 +33,8 @@ class TimetableRepository @Inject constructor(
local.getTimetable(semester, monday, friday) local.getTimetable(semester, monday, friday)
.toSingle(emptyList()) .toSingle(emptyList())
.doOnSuccess { old -> .doOnSuccess { old ->
local.deleteTimetable(old.uniqueSubtract(new)) local.deleteTimetable(old.uniqueSubtract(new).also { schedulerHelper.cancelScheduled(it) })
local.saveTimetable(new.uniqueSubtract(old).map { item -> local.saveTimetable(new.uniqueSubtract(old).also { schedulerHelper.scheduleNotifications(it, student) }.map { item ->
item.also { new -> item.also { new ->
old.singleOrNull { new.start == it.start }?.let { old -> old.singleOrNull { new.start == it.start }?.let { old ->
return@map new.copy( return@map new.copy(
@ -45,7 +47,7 @@ class TimetableRepository @Inject constructor(
} }
}.flatMap { }.flatMap {
local.getTimetable(semester, monday, friday).toSingle(emptyList()) local.getTimetable(semester, monday, friday).toSingle(emptyList())
}).map { list -> list.filter { it.date in start..end } } }).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 com.yariksoffice.lingver.Lingver
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import io.github.wulkanowy.WulkanowyApp import io.github.wulkanowy.WulkanowyApp
import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.SchedulersProvider import io.github.wulkanowy.utils.SchedulersProvider
@ -23,9 +21,6 @@ internal class AppModule {
@Provides @Provides
fun provideSchedulersProvider() = SchedulersProvider() fun provideSchedulersProvider() = SchedulersProvider()
@Provides
fun provideFlexibleAdapter() = FlexibleAdapter<AbstractFlexibleItem<*>>(null, null, true)
@Singleton @Singleton
@Provides @Provides
fun provideAppWidgetManager(context: Context): AppWidgetManager = AppWidgetManager.getInstance(context) fun provideAppWidgetManager(context: Context): AppWidgetManager = AppWidgetManager.getInstance(context)

View File

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

View File

@ -1,7 +1,9 @@
package io.github.wulkanowy.services package io.github.wulkanowy.services
import android.app.AlarmManager
import android.content.Context import android.content.Context
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.core.content.getSystemService
import androidx.work.WorkManager import androidx.work.WorkManager
import com.squareup.inject.assisted.dagger2.AssistedModule import com.squareup.inject.assisted.dagger2.AssistedModule
import dagger.Binds 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.NewGradesChannel
import io.github.wulkanowy.services.sync.channels.NewMessagesChannel import io.github.wulkanowy.services.sync.channels.NewMessagesChannel
import io.github.wulkanowy.services.sync.channels.NewNotesChannel 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.channels.PushChannel
import io.github.wulkanowy.services.sync.works.AttendanceSummaryWork import io.github.wulkanowy.services.sync.works.AttendanceSummaryWork
import io.github.wulkanowy.services.sync.works.AttendanceWork import io.github.wulkanowy.services.sync.works.AttendanceWork
import io.github.wulkanowy.services.sync.works.CompletedLessonWork import io.github.wulkanowy.services.sync.works.CompletedLessonWork
import io.github.wulkanowy.services.sync.works.ExamWork import io.github.wulkanowy.services.sync.works.ExamWork
import io.github.wulkanowy.services.sync.works.GradeStatisticsWork 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.GradeWork
import io.github.wulkanowy.services.sync.works.HomeworkWork import io.github.wulkanowy.services.sync.works.HomeworkWork
import io.github.wulkanowy.services.sync.works.LuckyNumberWork import io.github.wulkanowy.services.sync.works.LuckyNumberWork
@ -47,6 +49,10 @@ abstract class ServicesModule {
@Singleton @Singleton
@Provides @Provides
fun provideNotificationManager(context: Context) = NotificationManagerCompat.from(context) fun provideNotificationManager(context: Context) = NotificationManagerCompat.from(context)
@Singleton
@Provides
fun provideAlarmManager(context: Context): AlarmManager = context.getSystemService()!!
} }
@ContributesAndroidInjector @ContributesAndroidInjector
@ -64,10 +70,6 @@ abstract class ServicesModule {
@IntoSet @IntoSet
abstract fun provideAttendanceWork(work: AttendanceWork): Work abstract fun provideAttendanceWork(work: AttendanceWork): Work
@Binds
@IntoSet
abstract fun provideGradeSummaryWork(work: GradeSummaryWork): Work
@Binds @Binds
@IntoSet @IntoSet
abstract fun provideExamWork(work: ExamWork): Work abstract fun provideExamWork(work: ExamWork): Work
@ -131,4 +133,8 @@ abstract class ServicesModule {
@Binds @Binds
@IntoSet @IntoSet
abstract fun providePushChannel(channel: PushChannel): Channel 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_CANCEL_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_CANCEL_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_CANCEL_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 { init {
if (now().isHolidays) stopSyncWorker() if (now().isHolidays) stopSyncWorker()
if (SDK_INT > O) { if (SDK_INT >= O) {
channels.forEach { it.create() } channels.forEach { it.create() }
notificationManager.deleteNotificationChannel("new_entries_channel") notificationManager.deleteNotificationChannel("new_entries_channel")
} }

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

@ -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

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

View File

@ -1,9 +1,13 @@
package io.github.wulkanowy.ui.base package io.github.wulkanowy.ui.base
import android.widget.Toast import android.widget.Toast
import androidx.viewbinding.ViewBinding
import dagger.android.support.DaggerAppCompatDialogFragment 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) { override fun showError(text: String, error: Throwable) {
showMessage(text) showMessage(text)
@ -14,11 +18,11 @@ abstract class BaseDialogFragment : DaggerAppCompatDialogFragment(), BaseView {
} }
override fun showExpiredDialog() { override fun showExpiredDialog() {
(activity as? BaseActivity<*>)?.showExpiredDialog() (activity as? BaseActivity<*, *>)?.showExpiredDialog()
} }
override fun openClearLoginView() { override fun openClearLoginView() {
(activity as? BaseActivity<*>)?.openClearLoginView() (activity as? BaseActivity<*, *>)?.openClearLoginView()
} }
override fun showErrorDetailsDialog(error: Throwable) { 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 package io.github.wulkanowy.ui.base
import android.view.View 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
import com.google.android.material.snackbar.Snackbar.LENGTH_LONG import com.google.android.material.snackbar.Snackbar.LENGTH_LONG
import dagger.android.support.DaggerFragment import dagger.android.support.DaggerFragment
import io.github.wulkanowy.R 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 protected var messageContainer: View? = null
@ -16,7 +22,7 @@ abstract class BaseFragment : DaggerFragment(), BaseView {
.setAction(R.string.all_details) { if (isAdded) showErrorDetailsDialog(error) } .setAction(R.string.all_details) { if (isAdded) showErrorDetailsDialog(error) }
.show() .show()
} else { } 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) { if (messageContainer != null) {
Snackbar.make(messageContainer!!, text, LENGTH_LONG).show() Snackbar.make(messageContainer!!, text, LENGTH_LONG).show()
} else { } else {
(activity as? BaseActivity<*>)?.showMessage(text) (activity as? BaseActivity<*, *>)?.showMessage(text)
} }
} }
override fun showExpiredDialog() { override fun showExpiredDialog() {
(activity as? BaseActivity<*>)?.showExpiredDialog() (activity as? BaseActivity<*, *>)?.showExpiredDialog()
} }
override fun openClearLoginView() { override fun openClearLoginView() {
(activity as? BaseActivity<*>)?.openClearLoginView() (activity as? BaseActivity<*, *>)?.openClearLoginView()
} }
} }

View File

@ -11,6 +11,7 @@ import android.widget.Toast
import android.widget.Toast.LENGTH_LONG import android.widget.Toast.LENGTH_LONG
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.databinding.DialogErrorBinding
import io.github.wulkanowy.sdk.exception.FeatureDisabledException import io.github.wulkanowy.sdk.exception.FeatureDisabledException
import io.github.wulkanowy.sdk.exception.FeatureNotAvailableException import io.github.wulkanowy.sdk.exception.FeatureNotAvailableException
import io.github.wulkanowy.sdk.exception.ServiceUnavailableException import io.github.wulkanowy.sdk.exception.ServiceUnavailableException
@ -18,7 +19,6 @@ import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.getString import io.github.wulkanowy.utils.getString
import io.github.wulkanowy.utils.openEmailClient import io.github.wulkanowy.utils.openEmailClient
import io.github.wulkanowy.utils.openInternetBrowser import io.github.wulkanowy.utils.openInternetBrowser
import kotlinx.android.synthetic.main.dialog_error.*
import java.io.InterruptedIOException import java.io.InterruptedIOException
import java.io.PrintWriter import java.io.PrintWriter
import java.io.StringWriter import java.io.StringWriter
@ -26,7 +26,7 @@ import java.net.SocketTimeoutException
import java.net.UnknownHostException import java.net.UnknownHostException
import javax.inject.Inject import javax.inject.Inject
class ErrorDialog : BaseDialogFragment() { class ErrorDialog : BaseDialogFragment<DialogErrorBinding>() {
private lateinit var error: Throwable private lateinit var error: Throwable
@ -52,7 +52,7 @@ class ErrorDialog : BaseDialogFragment() {
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { 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?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
@ -62,6 +62,7 @@ class ErrorDialog : BaseDialogFragment() {
error.printStackTrace(PrintWriter(this)) error.printStackTrace(PrintWriter(this))
} }
with(binding) {
errorDialogContent.text = stringWriter.toString() errorDialogContent.text = stringWriter.toString()
with(errorDialogHorizontalScroll) { with(errorDialogHorizontalScroll) {
post { fullScroll(HorizontalScrollView.FOCUS_LEFT) } post { fullScroll(HorizontalScrollView.FOCUS_LEFT) }
@ -85,6 +86,7 @@ class ErrorDialog : BaseDialogFragment() {
else -> true else -> true
} }
} }
}
private fun openEmailClient(content: String) { private fun openEmailClient(content: String) {
requireContext().openEmailClient( requireContext().openEmailClient(

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.graphics.drawable.Drawable
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import androidx.recyclerview.widget.LinearLayoutManager
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.R
import io.github.wulkanowy.databinding.FragmentAboutBinding
import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.about.contributor.ContributorFragment import io.github.wulkanowy.ui.modules.about.contributor.ContributorFragment
import io.github.wulkanowy.ui.modules.about.license.LicenseFragment import io.github.wulkanowy.ui.modules.about.license.LicenseFragment
@ -19,17 +16,16 @@ import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.getCompatDrawable import io.github.wulkanowy.utils.getCompatDrawable
import io.github.wulkanowy.utils.openEmailClient import io.github.wulkanowy.utils.openEmailClient
import io.github.wulkanowy.utils.openInternetBrowser import io.github.wulkanowy.utils.openInternetBrowser
import io.github.wulkanowy.utils.setOnItemClickListener
import kotlinx.android.synthetic.main.fragment_about.*
import javax.inject.Inject import javax.inject.Inject
class AboutFragment : BaseFragment(), AboutView, MainView.TitledView { class AboutFragment : BaseFragment<FragmentAboutBinding>(R.layout.fragment_about), AboutView,
MainView.TitledView {
@Inject @Inject
lateinit var presenter: AboutPresenter lateinit var presenter: AboutPresenter
@Inject @Inject
lateinit var aboutAdapter: FlexibleAdapter<AbstractFlexibleItem<*>> lateinit var aboutAdapter: AboutAdapter
@Inject @Inject
lateinit var appInfo: AppInfo lateinit var appInfo: AppInfo
@ -80,29 +76,25 @@ class AboutFragment : BaseFragment(), AboutView, MainView.TitledView {
fun newInstance() = AboutFragment() 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?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
binding = FragmentAboutBinding.bind(view)
presenter.onAttachView(this) presenter.onAttachView(this)
} }
override fun initView() { override fun initView() {
aboutAdapter.setOnItemClickListener(presenter::onItemSelected) aboutAdapter.onClickListener = presenter::onItemSelected
with(aboutRecycler) { with(binding.aboutRecycler) {
layoutManager = SmoothScrollLinearLayoutManager(context) layoutManager = LinearLayoutManager(context)
adapter = aboutAdapter adapter = aboutAdapter
} }
} }
override fun updateData(header: AboutScrollableHeader, items: List<AboutItem>) { override fun updateData(data: List<Triple<String, String, Drawable?>>) {
with(aboutAdapter) { with(aboutAdapter) {
removeAllScrollableHeaders() items = data
addScrollableHeader(header) notifyDataSetChanged()
updateDataSet(items)
} }
} }

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,6 +1,5 @@
package io.github.wulkanowy.ui.modules.about 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.data.repositories.student.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.ui.base.ErrorHandler
@ -23,10 +22,9 @@ class AboutPresenter @Inject constructor(
loadData() loadData()
} }
fun onItemSelected(item: AbstractFlexibleItem<*>) { fun onItemSelected(name: String) {
if (item !is AboutItem) return
view?.run { view?.run {
when (item.title) { when (name) {
versionRes?.first -> { versionRes?.first -> {
Timber.i("Opening log viewer") Timber.i("Opening log viewer")
openLogViewer() openLogViewer()
@ -73,15 +71,16 @@ class AboutPresenter @Inject constructor(
private fun loadData() { private fun loadData() {
view?.run { view?.run {
updateData(AboutScrollableHeader(), listOfNotNull( updateData(listOfNotNull(
versionRes?.let { (title, summary, image) -> AboutItem(title, summary, image) }, versionRes,
creatorsRes?.let { (title, summary, image) -> AboutItem(title, summary, image) }, creatorsRes,
feedbackRes?.let { (title, summary, image) -> AboutItem(title, summary, image) }, feedbackRes,
faqRes?.let { (title, summary, image) -> AboutItem(title, summary, image) }, faqRes,
discordRes?.let { (title, summary, image) -> AboutItem(title, summary, image) }, discordRes,
homepageRes?.let { (title, summary, image) -> AboutItem(title, summary, image) }, homepageRes,
licensesRes?.let { (title, summary, image) -> AboutItem(title, summary, image) }, licensesRes,
privacyRes?.let { (title, summary, image) -> AboutItem(title, summary, image) })) 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,7 @@ interface AboutView : BaseView {
fun initView() fun initView()
fun updateData(header: AboutScrollableHeader, items: List<AboutItem>) fun updateData(data: List<Triple<String, String, Drawable?>>)
fun openLogViewer() 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 package io.github.wulkanowy.ui.modules.about.contributor
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.View.GONE import android.view.View.GONE
import android.view.View.VISIBLE import android.view.View.VISIBLE
import android.view.ViewGroup import androidx.recyclerview.widget.LinearLayoutManager
import eu.davidea.flexibleadapter.FlexibleAdapter
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.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.base.BaseFragment
import io.github.wulkanowy.ui.modules.main.MainView 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.openInternetBrowser
import io.github.wulkanowy.utils.setOnItemClickListener
import kotlinx.android.synthetic.main.fragment_creator.*
import javax.inject.Inject import javax.inject.Inject
class ContributorFragment : BaseFragment(), ContributorView, MainView.TitledView { class ContributorFragment : BaseFragment<FragmentContributorBinding>(R.layout.fragment_contributor),
ContributorView, MainView.TitledView {
@Inject @Inject
lateinit var presenter: ContributorPresenter lateinit var presenter: ContributorPresenter
@Inject @Inject
lateinit var creatorsAdapter: FlexibleAdapter<AbstractFlexibleItem<*>> lateinit var creatorsAdapter: ContributorAdapter
override val titleStringId get() = R.string.contributors_title override val titleStringId get() = R.string.contributors_title
@ -32,29 +29,27 @@ class ContributorFragment : BaseFragment(), ContributorView, MainView.TitledView
fun newInstance() = ContributorFragment() fun newInstance() = ContributorFragment()
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
return inflater.inflate(R.layout.fragment_creator, container, false) super.onViewCreated(view, savedInstanceState)
} binding = FragmentContributorBinding.bind(view)
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
presenter.onAttachView(this) presenter.onAttachView(this)
} }
override fun initView() { override fun initView() {
with(creatorRecycler) { with(binding.creatorRecycler) {
layoutManager = SmoothScrollLinearLayoutManager(context) layoutManager = LinearLayoutManager(context)
adapter = creatorsAdapter adapter = creatorsAdapter
addItemDecoration(FlexibleItemDecoration(context) addItemDecoration(DividerItemDecoration(context))
.withDefaultDivider()
.withDrawDividerOnLastItem(false))
} }
creatorsAdapter.setOnItemClickListener(presenter::onItemSelected) creatorsAdapter.onClickListener = presenter::onItemSelected
creatorSeeMore.setOnClickListener { presenter.onSeeMoreClick() } binding.creatorSeeMore.setOnClickListener { presenter.onSeeMoreClick() }
} }
override fun updateData(data: List<ContributorItem>) { override fun updateData(data: List<Contributor>) {
creatorsAdapter.updateDataSet(data) with(creatorsAdapter) {
items = data
notifyDataSetChanged()
}
} }
override fun openUserGithubPage(username: String) { override fun openUserGithubPage(username: String) {
@ -66,7 +61,7 @@ class ContributorFragment : BaseFragment(), ContributorView, MainView.TitledView
} }
override fun showProgress(show: Boolean) { override fun showProgress(show: Boolean) {
creatorProgress.visibility = if (show) VISIBLE else GONE binding.creatorProgress.visibility = if (show) VISIBLE else GONE
} }
override fun onDestroyView() { 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 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.appcreator.AppCreatorRepository
import io.github.wulkanowy.data.repositories.student.StudentRepository import io.github.wulkanowy.data.repositories.student.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.BasePresenter
@ -21,9 +21,8 @@ class ContributorPresenter @Inject constructor(
loadData() loadData()
} }
fun onItemSelected(item: AbstractFlexibleItem<*>) { fun onItemSelected(contributor: Contributor) {
if (item !is ContributorItem) return view?.openUserGithubPage(contributor.githubUsername)
view?.openUserGithubPage(item.creator.githubUsername)
} }
fun onSeeMoreClick() { fun onSeeMoreClick() {
@ -32,7 +31,6 @@ class ContributorPresenter @Inject constructor(
private fun loadData() { private fun loadData() {
disposable.add(appCreatorRepository.getAppCreators() disposable.add(appCreatorRepository.getAppCreators()
.map { it.map { creator -> ContributorItem(creator) } }
.subscribeOn(schedulers.backgroundThread) .subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread) .observeOn(schedulers.mainThread)
.doFinally { view?.showProgress(false) } .doFinally { view?.showProgress(false) }

View File

@ -1,12 +1,13 @@
package io.github.wulkanowy.ui.modules.about.contributor package io.github.wulkanowy.ui.modules.about.contributor
import io.github.wulkanowy.data.pojos.Contributor
import io.github.wulkanowy.ui.base.BaseView import io.github.wulkanowy.ui.base.BaseView
interface ContributorView : BaseView { interface ContributorView : BaseView {
fun initView() fun initView()
fun updateData(data: List<ContributorItem>) fun updateData(data: List<Contributor>)
fun openUserGithubPage(username: String) 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 package io.github.wulkanowy.ui.modules.about.license
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.View.GONE import android.view.View.GONE
import android.view.View.VISIBLE import android.view.View.VISIBLE
import android.view.ViewGroup
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.text.parseAsHtml import androidx.core.text.parseAsHtml
import androidx.recyclerview.widget.LinearLayoutManager
import com.mikepenz.aboutlibraries.Libs import com.mikepenz.aboutlibraries.Libs
import com.mikepenz.aboutlibraries.entity.Library import com.mikepenz.aboutlibraries.entity.Library
import dagger.Lazy 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.R
import io.github.wulkanowy.databinding.FragmentLicenseBinding
import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.main.MainView 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 import javax.inject.Inject
class LicenseFragment : BaseFragment(), LicenseView, MainView.TitledView { class LicenseFragment : BaseFragment<FragmentLicenseBinding>(R.layout.fragment_license),
LicenseView, MainView.TitledView {
@Inject @Inject
lateinit var presenter: LicensePresenter lateinit var presenter: LicensePresenter
@Inject @Inject
lateinit var licenseAdapter: FlexibleAdapter<AbstractFlexibleItem<*>> lateinit var licenseAdapter: LicenseAdapter
@Inject @Inject
lateinit var libs: Lazy<Libs> lateinit var libs: Lazy<Libs>
@ -43,25 +39,26 @@ class LicenseFragment : BaseFragment(), LicenseView, MainView.TitledView {
fun newInstance() = LicenseFragment() fun newInstance() = LicenseFragment()
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
return inflater.inflate(R.layout.fragment_license, container, false) super.onViewCreated(view, savedInstanceState)
} binding = FragmentLicenseBinding.bind(view)
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
presenter.onAttachView(this) presenter.onAttachView(this)
} }
override fun initView() { override fun initView() {
with(licenseRecycler) { licenseAdapter.onClickListener = presenter::onItemSelected
layoutManager = SmoothScrollLinearLayoutManager(context)
with(binding.licenseRecycler) {
layoutManager = LinearLayoutManager(context)
adapter = licenseAdapter adapter = licenseAdapter
} }
licenseAdapter.setOnItemClickListener(presenter::onItemSelected)
} }
override fun updateData(data: List<LicenseItem>) { override fun updateData(data: List<Library>) {
licenseAdapter.updateDataSet(data) with(licenseAdapter) {
items = data
notifyDataSetChanged()
}
} }
override fun openLicense(licenseHtml: String) { override fun openLicense(licenseHtml: String) {
@ -76,7 +73,7 @@ class LicenseFragment : BaseFragment(), LicenseView, MainView.TitledView {
} }
override fun showProgress(show: Boolean) { override fun showProgress(show: Boolean) {
licenseProgress.visibility = if (show) VISIBLE else GONE binding.licenseProgress.visibility = if (show) VISIBLE else GONE
} }
override fun onDestroyView() { 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 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.data.repositories.student.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.ui.base.ErrorHandler
@ -20,14 +20,12 @@ class LicensePresenter @Inject constructor(
loadData() loadData()
} }
fun onItemSelected(item: AbstractFlexibleItem<*>) { fun onItemSelected(library: Library) {
if (item !is LicenseItem) return view?.run { library.license?.licenseDescription?.let { openLicense(it) } }
view?.run { item.library.license?.licenseDescription?.let { openLicense(it) } }
} }
private fun loadData() { private fun loadData() {
disposable.add(Single.fromCallable { view?.appLibraries } disposable.add(Single.fromCallable { view?.appLibraries.orEmpty() }
.map { it.map { library -> LicenseItem(library) } }
.subscribeOn(schedulers.backgroundThread) .subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread) .observeOn(schedulers.mainThread)
.doOnEvent { _, _ -> view?.showProgress(false) } .doOnEvent { _, _ -> view?.showProgress(false) }

View File

@ -9,7 +9,7 @@ interface LicenseView : BaseView {
fun initView() fun initView()
fun updateData(data: List<LicenseItem>) fun updateData(data: List<Library>)
fun openLicense(licenseHtml: String) 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.SDK_INT
import android.os.Build.VERSION_CODES.LOLLIPOP import android.os.Build.VERSION_CODES.LOLLIPOP
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import io.github.wulkanowy.BuildConfig.APPLICATION_ID import io.github.wulkanowy.BuildConfig.APPLICATION_ID
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.databinding.FragmentLogviewerBinding
import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.modules.main.MainView
import kotlinx.android.synthetic.main.fragment_logviewer.*
import java.io.File import java.io.File
import javax.inject.Inject import javax.inject.Inject
class LogViewerFragment : BaseFragment(), LogViewerView, MainView.TitledView { class LogViewerFragment : BaseFragment<FragmentLogviewerBinding>(R.layout.fragment_logviewer),
LogViewerView, MainView.TitledView {
@Inject @Inject
lateinit var presenter: LogViewerPresenter lateinit var presenter: LogViewerPresenter
@ -43,13 +42,10 @@ class LogViewerFragment : BaseFragment(), LogViewerView, MainView.TitledView {
setHasOptionsMenu(true) setHasOptionsMenu(true)
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
return inflater.inflate(R.layout.fragment_logviewer, container, false) super.onViewCreated(view, savedInstanceState)
} binding = FragmentLogviewerBinding.bind(view)
messageContainer = binding.logViewerRecycler
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
messageContainer = logViewerRecycler
presenter.onAttachView(this) presenter.onAttachView(this)
} }
@ -63,18 +59,18 @@ class LogViewerFragment : BaseFragment(), LogViewerView, MainView.TitledView {
} }
override fun initView() { override fun initView() {
with(logViewerRecycler) { with(binding.logViewerRecycler) {
layoutManager = LinearLayoutManager(context) layoutManager = LinearLayoutManager(context)
adapter = logAdapter adapter = logAdapter
} }
logViewRefreshButton.setOnClickListener { presenter.onRefreshClick() } binding.logViewRefreshButton.setOnClickListener { presenter.onRefreshClick() }
} }
override fun setLines(lines: List<String>) { override fun setLines(lines: List<String>) {
logAdapter.lines = lines logAdapter.lines = lines
logAdapter.notifyDataSetChanged() logAdapter.notifyDataSetChanged()
logViewerRecycler.scrollToPosition(lines.size - 1) binding.logViewerRecycler.scrollToPosition(lines.size - 1)
} }
override fun shareLogs(files: List<File>) { 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
import android.widget.Toast.LENGTH_LONG import android.widget.Toast.LENGTH_LONG
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import eu.davidea.flexibleadapter.FlexibleAdapter import androidx.recyclerview.widget.LinearLayoutManager
import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import io.github.wulkanowy.R 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.base.BaseDialogFragment
import io.github.wulkanowy.ui.modules.login.LoginActivity 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 import javax.inject.Inject
class AccountDialog : BaseDialogFragment(), AccountView { class AccountDialog : BaseDialogFragment<DialogAccountBinding>(), AccountView {
@Inject @Inject
lateinit var presenter: AccountPresenter lateinit var presenter: AccountPresenter
@Inject @Inject
lateinit var accountAdapter: FlexibleAdapter<AbstractFlexibleItem<*>> lateinit var accountAdapter: AccountAdapter
companion object { companion object {
fun newInstance() = AccountDialog() fun newInstance() = AccountDialog()
@ -35,7 +33,7 @@ class AccountDialog : BaseDialogFragment(), AccountView {
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { 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?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
@ -44,18 +42,23 @@ class AccountDialog : BaseDialogFragment(), AccountView {
} }
override fun initView() { override fun initView() {
accountAdapter.setOnItemClickListener { presenter.onItemSelected(it) } accountAdapter.onClickListener = presenter::onItemSelected
with(binding) {
accountDialogAdd.setOnClickListener { presenter.onAddSelected() } accountDialogAdd.setOnClickListener { presenter.onAddSelected() }
accountDialogRemove.setOnClickListener { presenter.onRemoveSelected() } accountDialogRemove.setOnClickListener { presenter.onRemoveSelected() }
accountDialogRecycler.apply { accountDialogRecycler.apply {
layoutManager = SmoothScrollLinearLayoutManager(context) layoutManager = LinearLayoutManager(context)
adapter = accountAdapter adapter = accountAdapter
} }
} }
}
override fun updateData(data: List<AccountItem>) { override fun updateData(data: List<Student>) {
accountAdapter.updateDataSet(data) with(accountAdapter) {
items = data
notifyDataSetChanged()
}
} }
override fun showError(text: String, error: Throwable) { 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 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.data.repositories.student.StudentRepository
import io.github.wulkanowy.services.sync.SyncManager import io.github.wulkanowy.services.sync.SyncManager
import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.BasePresenter
@ -63,14 +63,13 @@ class AccountPresenter @Inject constructor(
})) }))
} }
fun onItemSelected(item: AbstractFlexibleItem<*>) { fun onItemSelected(student: Student) {
if (item is AccountItem) { Timber.i("Select student item ${student.id}")
Timber.i("Select student item ${item.student.id}") if (student.isCurrent) {
if (item.student.isCurrent) {
view?.dismissView() view?.dismissView()
} else { } else {
Timber.i("Attempt to change a student") Timber.i("Attempt to change a student")
disposable.add(studentRepository.switchStudent(item.student) disposable.add(studentRepository.switchStudent(student)
.subscribeOn(schedulers.backgroundThread) .subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread) .observeOn(schedulers.mainThread)
.doFinally { view?.dismissView() } .doFinally { view?.dismissView() }
@ -83,12 +82,10 @@ class AccountPresenter @Inject constructor(
})) }))
} }
} }
}
private fun loadData() { private fun loadData() {
Timber.i("Loading account data started") Timber.i("Loading account data started")
disposable.add(studentRepository.getSavedStudents(false) disposable.add(studentRepository.getSavedStudents(false)
.map { it.map { item -> AccountItem(item) } }
.subscribeOn(schedulers.backgroundThread) .subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread) .observeOn(schedulers.mainThread)
.subscribe({ .subscribe({
@ -100,4 +97,3 @@ class AccountPresenter @Inject constructor(
})) }))
} }
} }

View File

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

View File

@ -1,12 +1,80 @@
package io.github.wulkanowy.ui.modules.attendance package io.github.wulkanowy.ui.modules.attendance
import eu.davidea.flexibleadapter.FlexibleAdapter import android.view.LayoutInflater
import eu.davidea.flexibleadapter.items.IFlexible 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.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 excuseActionMode: Boolean = false
var onClickListener: (Attendance) -> Unit = {}
var onExcuseCheckboxSelect: (attendanceItem: Attendance, checked: Boolean) -> 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.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Attendance import io.github.wulkanowy.data.db.entities.Attendance
import io.github.wulkanowy.databinding.DialogAttendanceBinding
import io.github.wulkanowy.utils.lifecycleAwareVariable
import io.github.wulkanowy.utils.toFormattedString import io.github.wulkanowy.utils.toFormattedString
import kotlinx.android.synthetic.main.dialog_attendance.*
class AttendanceDialog : DialogFragment() { class AttendanceDialog : DialogFragment() {
private var binding: DialogAttendanceBinding by lifecycleAwareVariable()
private lateinit var attendance: Attendance private lateinit var attendance: Attendance
companion object { companion object {
@ -33,16 +35,18 @@ class AttendanceDialog : DialogFragment() {
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { 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?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onActivityCreated(savedInstanceState)
with(binding) {
attendanceDialogSubject.text = attendance.subject attendanceDialogSubject.text = attendance.subject
attendanceDialogDescription.text = attendance.name attendanceDialogDescription.text = attendance.name
attendanceDialogDate.text = attendance.date.toFormattedString() attendanceDialogDate.text = attendance.date.toFormattedString()
attendanceDialogNumber.text = attendance.number.toString() attendanceDialogNumber.text = attendance.number.toString()
attendanceDialogClose.setOnClickListener { dismiss() } attendanceDialogClose.setOnClickListener { dismiss() }
} }
}
} }

View File

@ -10,35 +10,32 @@ import android.view.View
import android.view.View.GONE import android.view.View.GONE
import android.view.View.INVISIBLE import android.view.View.INVISIBLE
import android.view.View.VISIBLE import android.view.View.VISIBLE
import android.view.ViewGroup
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.recyclerview.widget.LinearLayoutManager
import com.wdullaer.materialdatetimepicker.date.DatePickerDialog 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.R
import io.github.wulkanowy.data.db.entities.Attendance 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.base.BaseFragment
import io.github.wulkanowy.ui.widgets.DividerItemDecoration
import io.github.wulkanowy.ui.modules.attendance.summary.AttendanceSummaryFragment import io.github.wulkanowy.ui.modules.attendance.summary.AttendanceSummaryFragment
import io.github.wulkanowy.ui.modules.main.MainActivity import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.SchooldaysRangeLimiter import io.github.wulkanowy.utils.SchooldaysRangeLimiter
import io.github.wulkanowy.utils.dpToPx 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 org.threeten.bp.LocalDate
import javax.inject.Inject import javax.inject.Inject
class AttendanceFragment : BaseFragment(), AttendanceView, MainView.MainChildView, class AttendanceFragment : BaseFragment<FragmentAttendanceBinding>(R.layout.fragment_attendance), AttendanceView, MainView.MainChildView,
MainView.TitledView { MainView.TitledView {
@Inject @Inject
lateinit var presenter: AttendancePresenter lateinit var presenter: AttendancePresenter
@Inject @Inject
lateinit var attendanceAdapter: AttendanceAdapter<AbstractFlexibleItem<*>> lateinit var attendanceAdapter: AttendanceAdapter
override val excuseSuccessString: String override val excuseSuccessString: String
get() = getString(R.string.attendance_excuse_success) 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 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 override val currentStackSize get() = (activity as? MainActivity)?.currentStackSize
@ -91,28 +88,26 @@ class AttendanceFragment : BaseFragment(), AttendanceView, MainView.MainChildVie
setHasOptionsMenu(true) setHasOptionsMenu(true)
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
return inflater.inflate(R.layout.fragment_attendance, container, false) super.onViewCreated(view, savedInstanceState)
} binding = FragmentAttendanceBinding.bind(view)
messageContainer = binding.attendanceRecycler
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
messageContainer = attendanceRecycler
presenter.onAttachView(this, savedInstanceState?.getLong(SAVED_DATE_KEY)) presenter.onAttachView(this, savedInstanceState?.getLong(SAVED_DATE_KEY))
} }
override fun initView() { override fun initView() {
attendanceAdapter.setOnItemClickListener(presenter::onAttendanceItemSelected) with(attendanceAdapter) {
attendanceAdapter.onExcuseCheckboxSelect = presenter::onExcuseCheckboxSelect onClickListener = presenter::onAttendanceItemSelected
onExcuseCheckboxSelect = presenter::onExcuseCheckboxSelect
with(attendanceRecycler) {
layoutManager = SmoothScrollLinearLayoutManager(context)
adapter = attendanceAdapter
addItemDecoration(FlexibleItemDecoration(context)
.withDefaultDivider()
.withDrawDividerOnLastItem(false))
} }
with(binding.attendanceRecycler) {
layoutManager = LinearLayoutManager(context)
adapter = attendanceAdapter
addItemDecoration(DividerItemDecoration(context))
}
with(binding) {
attendanceSwipe.setOnRefreshListener(presenter::onSwipeRefresh) attendanceSwipe.setOnRefreshListener(presenter::onSwipeRefresh)
attendanceErrorRetry.setOnClickListener { presenter.onRetry() } attendanceErrorRetry.setOnClickListener { presenter.onRetry() }
attendanceErrorDetails.setOnClickListener { presenter.onDetailsClick() } attendanceErrorDetails.setOnClickListener { presenter.onDetailsClick() }
@ -125,6 +120,7 @@ class AttendanceFragment : BaseFragment(), AttendanceView, MainView.MainChildVie
attendanceNavContainer.setElevationCompat(requireContext().dpToPx(8f)) attendanceNavContainer.setElevationCompat(requireContext().dpToPx(8f))
} }
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.action_menu_attendance, menu) inflater.inflate(R.menu.action_menu_attendance, menu)
@ -135,20 +131,26 @@ class AttendanceFragment : BaseFragment(), AttendanceView, MainView.MainChildVie
else false else false
} }
override fun updateData(data: List<AttendanceItem>) { override fun updateData(data: List<Attendance>) {
attendanceAdapter.updateDataSet(data, true) with(attendanceAdapter) {
items = data
notifyDataSetChanged()
}
} }
override fun updateNavigationDay(date: String) { override fun updateNavigationDay(date: String) {
attendanceNavDate.text = date binding.attendanceNavDate.text = date
} }
override fun clearData() { override fun clearData() {
attendanceAdapter.clear() with(attendanceAdapter) {
items = emptyList()
notifyDataSetChanged()
}
} }
override fun resetView() { override fun resetView() {
attendanceRecycler.smoothScrollToPosition(0) binding.attendanceRecycler.smoothScrollToPosition(0)
} }
override fun onFragmentReselected() { override fun onFragmentReselected() {
@ -164,43 +166,43 @@ class AttendanceFragment : BaseFragment(), AttendanceView, MainView.MainChildVie
} }
override fun showEmpty(show: Boolean) { 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) { 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) { override fun setErrorDetails(message: String) {
attendanceErrorMessage.text = message binding.attendanceErrorMessage.text = message
} }
override fun showProgress(show: Boolean) { 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) { override fun enableSwipe(enable: Boolean) {
attendanceSwipe.isEnabled = enable binding.attendanceSwipe.isEnabled = enable
} }
override fun showContent(show: Boolean) { override fun showContent(show: Boolean) {
attendanceRecycler.visibility = if (show) VISIBLE else GONE binding. attendanceRecycler.visibility = if (show) VISIBLE else GONE
} }
override fun hideRefresh() { override fun hideRefresh() {
attendanceSwipe.isRefreshing = false binding.attendanceSwipe.isRefreshing = false
} }
override fun showPreButton(show: Boolean) { 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) { 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) { 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) { override fun showAttendanceDialog(lesson: Attendance) {
@ -223,14 +225,15 @@ class AttendanceFragment : BaseFragment(), AttendanceView, MainView.MainChildVie
} }
override fun showExcuseDialog() { override fun showExcuseDialog() {
val dialogBinding = DialogExcuseBinding.inflate(LayoutInflater.from(context))
AlertDialog.Builder(requireContext()) AlertDialog.Builder(requireContext())
.setTitle(R.string.attendance_excuse_title) .setTitle(R.string.attendance_excuse_title)
.setView(R.layout.dialog_excuse) .setView(dialogBinding.root)
.setNegativeButton(android.R.string.cancel) { _, _ -> } .setNegativeButton(android.R.string.cancel) { _, _ -> }
.create() .create()
.apply { .apply {
setButton(BUTTON_POSITIVE, getString(R.string.attendance_excuse_dialog_submit)) { _, _ -> setButton(BUTTON_POSITIVE, getString(R.string.attendance_excuse_dialog_submit)) { _, _ ->
presenter.onExcuseDialogSubmit(excuseReason.text?.toString().orEmpty()) presenter.onExcuseDialogSubmit(dialogBinding.excuseReason.text?.toString().orEmpty())
} }
}.show() }.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 package io.github.wulkanowy.ui.modules.attendance
import android.annotation.SuppressLint import android.annotation.SuppressLint
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import io.github.wulkanowy.data.db.entities.Attendance import io.github.wulkanowy.data.db.entities.Attendance
import io.github.wulkanowy.data.repositories.attendance.AttendanceRepository import io.github.wulkanowy.data.repositories.attendance.AttendanceRepository
import io.github.wulkanowy.data.repositories.preferences.PreferencesRepository import io.github.wulkanowy.data.repositories.preferences.PreferencesRepository
@ -111,11 +110,11 @@ class AttendancePresenter @Inject constructor(
view?.finishActionMode() view?.finishActionMode()
} }
fun onAttendanceItemSelected(item: AbstractFlexibleItem<*>?) { fun onAttendanceItemSelected(attendance: Attendance) {
view?.apply { view?.apply {
if (item is AttendanceItem && !excuseActionMode) { if (!excuseActionMode) {
Timber.i("Select attendance item ${item.attendance.id}") Timber.i("Select attendance item ${attendance.id}")
showAttendanceDialog(item.attendance) showAttendanceDialog(attendance)
} }
} }
} }
@ -197,9 +196,7 @@ class AttendancePresenter @Inject constructor(
if (prefRepository.isShowPresent) list if (prefRepository.isShowPresent) list
else list.filter { !it.presence } else list.filter { !it.presence }
} }
.delay(200, MILLISECONDS) .map { items -> items.sortedBy { it.number } }
.map { items -> items.map { AttendanceItem(it) } }
.map { items -> items.sortedBy { it.attendance.number } }
.subscribeOn(schedulers.backgroundThread) .subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread) .observeOn(schedulers.mainThread)
.doFinally { .doFinally {
@ -216,7 +213,7 @@ class AttendancePresenter @Inject constructor(
showEmpty(it.isEmpty()) showEmpty(it.isEmpty())
showErrorView(false) showErrorView(false)
showContent(it.isNotEmpty()) 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_attendance", "items" to it.size, "force_refresh" to forceRefresh)
}) { }) {
@ -236,7 +233,6 @@ class AttendancePresenter @Inject constructor(
attendanceRepository.excuseForAbsence(student, semester, toExcuseList, reason) attendanceRepository.excuseForAbsence(student, semester, toExcuseList, reason)
} }
} }
.delay(200, MILLISECONDS)
.subscribeOn(schedulers.backgroundThread) .subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread) .observeOn(schedulers.mainThread)
.doOnSubscribe { .doOnSubscribe {

View File

@ -18,7 +18,7 @@ interface AttendanceView : BaseView {
fun initView() fun initView()
fun updateData(data: List<AttendanceItem>) fun updateData(data: List<Attendance>)
fun updateNavigationDay(date: String) 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 package io.github.wulkanowy.ui.modules.attendance.summary
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.View.GONE import android.view.View.GONE
import android.view.View.INVISIBLE import android.view.View.INVISIBLE
import android.view.View.VISIBLE import android.view.View.VISIBLE
import android.view.ViewGroup
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.TextView import android.widget.TextView
import eu.davidea.flexibleadapter.FlexibleAdapter import androidx.recyclerview.widget.LinearLayoutManager
import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import io.github.wulkanowy.R 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.base.BaseFragment
import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.dpToPx import io.github.wulkanowy.utils.dpToPx
import io.github.wulkanowy.utils.setOnItemSelectedListener import io.github.wulkanowy.utils.setOnItemSelectedListener
import kotlinx.android.synthetic.main.fragment_attendance_summary.*
import javax.inject.Inject import javax.inject.Inject
class AttendanceSummaryFragment : BaseFragment(), AttendanceSummaryView, MainView.TitledView { class AttendanceSummaryFragment :
BaseFragment<FragmentAttendanceSummaryBinding>(R.layout.fragment_attendance_summary),
AttendanceSummaryView, MainView.TitledView {
@Inject @Inject
lateinit var presenter: AttendanceSummaryPresenter lateinit var presenter: AttendanceSummaryPresenter
@Inject @Inject
lateinit var attendanceSummaryAdapter: FlexibleAdapter<AbstractFlexibleItem<*>> lateinit var attendanceSummaryAdapter: AttendanceSummaryAdapter
private lateinit var subjectsAdapter: ArrayAdapter<String> private lateinit var subjectsAdapter: ArrayAdapter<String>
@ -36,41 +35,38 @@ class AttendanceSummaryFragment : BaseFragment(), AttendanceSummaryView, MainVie
fun newInstance() = AttendanceSummaryFragment() fun newInstance() = AttendanceSummaryFragment()
} }
override val totalString get() = getString(R.string.attendance_summary_total)
override val titleStringId get() = R.string.attendance_title 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? { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
return inflater.inflate(R.layout.fragment_attendance_summary, container, false) super.onViewCreated(view, savedInstanceState)
} binding = FragmentAttendanceSummaryBinding.bind(view)
messageContainer = binding.attendanceSummaryRecycler
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
messageContainer = attendanceSummaryRecycler
presenter.onAttachView(this, savedInstanceState?.getInt(SAVED_SUBJECT_KEY)) presenter.onAttachView(this, savedInstanceState?.getInt(SAVED_SUBJECT_KEY))
} }
override fun initView() { override fun initView() {
with(attendanceSummaryRecycler) { with(binding.attendanceSummaryRecycler) {
layoutManager = SmoothScrollLinearLayoutManager(context) layoutManager = LinearLayoutManager(context)
adapter = attendanceSummaryAdapter adapter = attendanceSummaryAdapter
} }
with(binding) {
attendanceSummarySwipe.setOnRefreshListener(presenter::onSwipeRefresh) attendanceSummarySwipe.setOnRefreshListener(presenter::onSwipeRefresh)
attendanceSummaryErrorRetry.setOnClickListener { presenter.onRetry() } attendanceSummaryErrorRetry.setOnClickListener { presenter.onRetry() }
attendanceSummaryErrorDetails.setOnClickListener { presenter.onDetailsClick() } attendanceSummaryErrorDetails.setOnClickListener { presenter.onDetailsClick() }
}
subjectsAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, mutableListOf()) subjectsAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, mutableListOf())
subjectsAdapter.setDropDownViewResource(R.layout.item_attendance_summary_subject) subjectsAdapter.setDropDownViewResource(R.layout.item_attendance_summary_subject)
with(attendanceSummarySubjects) { with(binding.attendanceSummarySubjects) {
adapter = subjectsAdapter adapter = subjectsAdapter
setOnItemSelectedListener<TextView> { presenter.onSubjectSelected(it?.text?.toString()) } setOnItemSelectedListener<TextView> { presenter.onSubjectSelected(it?.text?.toString()) }
} }
attendanceSummarySubjectsContainer.setElevationCompat(requireContext().dpToPx(1f)) binding.attendanceSummarySubjectsContainer.setElevationCompat(requireContext().dpToPx(1f))
} }
override fun updateSubjects(data: ArrayList<String>) { 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) { with(attendanceSummaryAdapter) {
updateDataSet(data, true) items = data
removeAllScrollableHeaders() notifyDataSetChanged()
addScrollableHeader(header)
} }
} }
override fun clearView() { override fun clearView() {
attendanceSummaryAdapter.clear() with(attendanceSummaryAdapter) {
items = emptyList()
notifyDataSetChanged()
}
} }
override fun showEmpty(show: Boolean) { 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) { 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) { override fun setErrorDetails(message: String) {
attendanceSummaryErrorMessage.text = message binding.attendanceSummaryErrorMessage.text = message
} }
override fun showProgress(show: Boolean) { 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) { override fun enableSwipe(enable: Boolean) {
attendanceSummarySwipe.isEnabled = enable binding.attendanceSummarySwipe.isEnabled = enable
} }
override fun showContent(show: Boolean) { 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) { override fun showSubjects(show: Boolean) {
attendanceSummarySubjectsContainer.visibility = if (show) VISIBLE else INVISIBLE binding.attendanceSummarySubjectsContainer.visibility = if (show) VISIBLE else INVISIBLE
} }
override fun hideRefresh() { override fun hideRefresh() {
attendanceSummarySwipe.isRefreshing = false binding.attendanceSummarySwipe.isRefreshing = false
} }
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
@ -131,7 +129,7 @@ class AttendanceSummaryFragment : BaseFragment(), AttendanceSummaryView, MainVie
} }
override fun onDestroyView() { override fun onDestroyView() {
super.onDestroyView()
presenter.onDetachView() 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 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.db.entities.Subject
import io.github.wulkanowy.data.repositories.attendancesummary.AttendanceSummaryRepository import io.github.wulkanowy.data.repositories.attendancesummary.AttendanceSummaryRepository
import io.github.wulkanowy.data.repositories.semester.SemesterRepository 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.ui.base.ErrorHandler
import io.github.wulkanowy.utils.FirebaseAnalyticsHelper import io.github.wulkanowy.utils.FirebaseAnalyticsHelper
import io.github.wulkanowy.utils.SchedulersProvider import io.github.wulkanowy.utils.SchedulersProvider
import io.github.wulkanowy.utils.calculatePercentage
import io.github.wulkanowy.utils.getFormattedName
import org.threeten.bp.Month import org.threeten.bp.Month
import timber.log.Timber import timber.log.Timber
import java.lang.String.format
import java.util.Locale.FRANCE
import java.util.concurrent.TimeUnit.MILLISECONDS
import javax.inject.Inject import javax.inject.Inject
class AttendanceSummaryPresenter @Inject constructor( class AttendanceSummaryPresenter @Inject constructor(
@ -88,8 +82,7 @@ class AttendanceSummaryPresenter @Inject constructor(
attendanceSummaryRepository.getAttendanceSummary(student, it, subjectId, forceRefresh) attendanceSummaryRepository.getAttendanceSummary(student, it, subjectId, forceRefresh)
} }
} }
.map { createAttendanceSummaryItems(it) to AttendanceSummaryScrollableHeader(formatPercentage(it.calculatePercentage())) } .map { items -> items.sortedByDescending { if (it.month.value <= Month.JUNE.value) it.month.value + 12 else it.month.value } }
.delay(200, MILLISECONDS)
.subscribeOn(schedulers.backgroundThread) .subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread) .observeOn(schedulers.mainThread)
.doFinally { .doFinally {
@ -102,11 +95,11 @@ class AttendanceSummaryPresenter @Inject constructor(
.subscribe({ .subscribe({
Timber.i("Loading attendance summary result: Success") Timber.i("Loading attendance summary result: Success")
view?.apply { view?.apply {
showEmpty(it.first.isEmpty()) showEmpty(it.isEmpty())
showContent(it.first.isNotEmpty()) showContent(it.isNotEmpty())
updateDataSet(it.first, it.second) updateDataSet(it)
} }
analytics.logEvent("load_attendance_summary", "items" to it.first.size, "force_refresh" to forceRefresh, "item_id" to subjectId) analytics.logEvent("load_attendance_summary", "items" to it.size, "force_refresh" to forceRefresh, "item_id" to subjectId)
}) { }) {
Timber.i("Loading attendance summary result: An exception occurred") Timber.i("Loading attendance summary result: An exception occurred")
errorHandler.dispatch(it) errorHandler.dispatch(it)
@ -150,42 +143,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 package io.github.wulkanowy.ui.modules.attendance.summary
import io.github.wulkanowy.data.db.entities.AttendanceSummary
import io.github.wulkanowy.ui.base.BaseView import io.github.wulkanowy.ui.base.BaseView
interface AttendanceSummaryView : BaseView { interface AttendanceSummaryView : BaseView {
val totalString: String
val isViewEmpty: Boolean val isViewEmpty: Boolean
fun initView() fun initView()
@ -24,7 +23,7 @@ interface AttendanceSummaryView : BaseView {
fun setErrorDetails(message: String) fun setErrorDetails(message: String)
fun updateDataSet(data: List<AttendanceSummaryItem>, header: AttendanceSummaryScrollableHeader) fun updateDataSet(data: List<AttendanceSummary>)
fun updateSubjects(data: ArrayList<String>) 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.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Exam import io.github.wulkanowy.data.db.entities.Exam
import io.github.wulkanowy.databinding.DialogExamBinding
import io.github.wulkanowy.utils.lifecycleAwareVariable
import io.github.wulkanowy.utils.toFormattedString import io.github.wulkanowy.utils.toFormattedString
import kotlinx.android.synthetic.main.dialog_exam.*
class ExamDialog : DialogFragment() { class ExamDialog : DialogFragment() {
private var binding: DialogExamBinding by lifecycleAwareVariable()
private lateinit var exam: Exam private lateinit var exam: Exam
companion object { companion object {
@ -33,12 +35,13 @@ class ExamDialog : DialogFragment() {
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { 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?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onActivityCreated(savedInstanceState)
with(binding) {
examDialogSubjectValue.text = exam.subject examDialogSubjectValue.text = exam.subject
examDialogTypeValue.text = exam.type examDialogTypeValue.text = exam.type
examDialogTeacherValue.text = exam.teacher examDialogTeacherValue.text = exam.teacher
@ -47,4 +50,5 @@ class ExamDialog : DialogFragment() {
examDialogClose.setOnClickListener { dismiss() } examDialogClose.setOnClickListener { dismiss() }
} }
}
} }

View File

@ -1,33 +1,29 @@
package io.github.wulkanowy.ui.modules.exam package io.github.wulkanowy.ui.modules.exam
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.View.GONE import android.view.View.GONE
import android.view.View.INVISIBLE import android.view.View.INVISIBLE
import android.view.View.VISIBLE import android.view.View.VISIBLE
import android.view.ViewGroup import androidx.recyclerview.widget.LinearLayoutManager
import eu.davidea.flexibleadapter.FlexibleAdapter
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.R
import io.github.wulkanowy.data.db.entities.Exam 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.base.BaseFragment
import io.github.wulkanowy.ui.modules.main.MainActivity import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.ui.widgets.DividerItemDecoration
import io.github.wulkanowy.utils.dpToPx import io.github.wulkanowy.utils.dpToPx
import io.github.wulkanowy.utils.setOnItemClickListener
import kotlinx.android.synthetic.main.fragment_exam.*
import javax.inject.Inject 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 @Inject
lateinit var presenter: ExamPresenter lateinit var presenter: ExamPresenter
@Inject @Inject
lateinit var examAdapter: FlexibleAdapter<AbstractFlexibleItem<*>> lateinit var examAdapter: ExamAdapter
companion object { companion object {
private const val SAVED_DATE_KEY = "CURRENT_DATE" private const val SAVED_DATE_KEY = "CURRENT_DATE"
@ -37,29 +33,25 @@ class ExamFragment : BaseFragment(), ExamView, MainView.MainChildView, MainView.
override val titleStringId get() = R.string.exam_title 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? { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
return inflater.inflate(R.layout.fragment_exam, container, false) super.onViewCreated(view, savedInstanceState)
} binding = FragmentExamBinding.bind(view)
messageContainer = binding.examRecycler
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
messageContainer = examRecycler
presenter.onAttachView(this, savedInstanceState?.getLong(SAVED_DATE_KEY)) presenter.onAttachView(this, savedInstanceState?.getLong(SAVED_DATE_KEY))
} }
override fun initView() { override fun initView() {
examAdapter.setOnItemClickListener(presenter::onExamItemSelected) examAdapter.onClickListener = presenter::onExamItemSelected
with(examRecycler) { with(binding.examRecycler) {
layoutManager = SmoothScrollLinearLayoutManager(context) layoutManager = LinearLayoutManager(context)
adapter = examAdapter adapter = examAdapter
addItemDecoration(FlexibleItemDecoration(context) addItemDecoration(DividerItemDecoration(context))
.withDefaultDivider(R.layout.item_exam)
.withDrawDividerOnLastItem(false))
} }
with(binding) {
examSwipe.setOnRefreshListener(presenter::onSwipeRefresh) examSwipe.setOnRefreshListener(presenter::onSwipeRefresh)
examErrorRetry.setOnClickListener { presenter.onRetry() } examErrorRetry.setOnClickListener { presenter.onRetry() }
examErrorDetails.setOnClickListener { presenter.onDetailsClick() } examErrorDetails.setOnClickListener { presenter.onDetailsClick() }
@ -69,25 +61,32 @@ class ExamFragment : BaseFragment(), ExamView, MainView.MainChildView, MainView.
examNavContainer.setElevationCompat(requireContext().dpToPx(8f)) examNavContainer.setElevationCompat(requireContext().dpToPx(8f))
} }
override fun hideRefresh() {
examSwipe.isRefreshing = false
} }
override fun updateData(data: List<ExamItem>) { override fun hideRefresh() {
examAdapter.updateDataSet(data, true) binding.examSwipe.isRefreshing = false
}
override fun updateData(data: List<ExamItem<*>>) {
with(examAdapter) {
items = data
notifyDataSetChanged()
}
} }
override fun updateNavigationWeek(date: String) { override fun updateNavigationWeek(date: String) {
examNavDate.text = date binding.examNavDate.text = date
} }
override fun clearData() { override fun clearData() {
examAdapter.clear() with(examAdapter) {
items = emptyList()
notifyDataSetChanged()
}
} }
override fun resetView() { override fun resetView() {
examRecycler.scrollToPosition(0) binding.examRecycler.scrollToPosition(0)
} }
override fun onFragmentReselected() { override fun onFragmentReselected() {
@ -95,35 +94,35 @@ class ExamFragment : BaseFragment(), ExamView, MainView.MainChildView, MainView.
} }
override fun showEmpty(show: Boolean) { 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) { 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) { override fun setErrorDetails(message: String) {
examErrorMessage.text = message binding.examErrorMessage.text = message
} }
override fun showProgress(show: Boolean) { 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) { override fun enableSwipe(enable: Boolean) {
examSwipe.isEnabled = enable binding.examSwipe.isEnabled = enable
} }
override fun showContent(show: Boolean) { 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) { 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) { 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) { 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 package io.github.wulkanowy.ui.modules.exam
import android.view.View data class ExamItem<out T>(val value: T, val viewType: ViewType) {
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.*
class ExamItem(header: ExamHeader, val exam: Exam) : AbstractSectionableItem<ExamItem.ViewHolder, ExamHeader>(header) { enum class ViewType(val id: Int) {
HEADER(1),
override fun getLayoutRes() = R.layout.item_exam ITEM(2)
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
} }
} }

View File

@ -1,6 +1,5 @@
package io.github.wulkanowy.ui.modules.exam 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.db.entities.Exam
import io.github.wulkanowy.data.repositories.exam.ExamRepository import io.github.wulkanowy.data.repositories.exam.ExamRepository
import io.github.wulkanowy.data.repositories.semester.SemesterRepository import io.github.wulkanowy.data.repositories.semester.SemesterRepository
@ -19,7 +18,6 @@ import org.threeten.bp.LocalDate
import org.threeten.bp.LocalDate.now import org.threeten.bp.LocalDate.now
import org.threeten.bp.LocalDate.ofEpochDay import org.threeten.bp.LocalDate.ofEpochDay
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.TimeUnit.MILLISECONDS
import javax.inject.Inject import javax.inject.Inject
class ExamPresenter @Inject constructor( class ExamPresenter @Inject constructor(
@ -75,11 +73,9 @@ class ExamPresenter @Inject constructor(
view?.showErrorDetailsDialog(lastError) view?.showErrorDetailsDialog(lastError)
} }
fun onExamItemSelected(item: AbstractFlexibleItem<*>?) { fun onExamItemSelected(exam: Exam) {
if (item is ExamItem) { Timber.i("Select exam item ${exam.id}")
Timber.i("Select exam item ${item.exam.id}") view?.showExamDialog(exam)
view?.showExamDialog(item.exam)
}
} }
fun onViewReselected() { fun onViewReselected() {
@ -117,8 +113,6 @@ class ExamPresenter @Inject constructor(
examRepository.getExams(student, semester, currentDate.monday, currentDate.friday, forceRefresh) examRepository.getExams(student, semester, currentDate.monday, currentDate.friday, forceRefresh)
} }
} }
.delay(200, MILLISECONDS)
.map { it.groupBy { exam -> exam.date }.toSortedMap() }
.map { createExamItems(it) } .map { createExamItems(it) }
.subscribeOn(schedulers.backgroundThread) .subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread) .observeOn(schedulers.mainThread)
@ -156,12 +150,12 @@ class ExamPresenter @Inject constructor(
} }
} }
private fun createExamItems(items: Map<LocalDate, List<Exam>>): List<ExamItem> { private fun createExamItems(items: List<Exam>): List<ExamItem<*>> {
return items.flatMap { return items.groupBy { it.date }.toSortedMap().map { (date, exams) ->
ExamHeader(it.key).let { header -> listOf(ExamItem(date, ExamItem.ViewType.HEADER)) + exams.reversed().map { exam ->
it.value.reversed().map { item -> ExamItem(header, item) } ExamItem(exam, ExamItem.ViewType.ITEM)
}
} }
}.flatten()
} }
private fun reloadView() { private fun reloadView() {

View File

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

View File

@ -3,71 +3,76 @@ package io.github.wulkanowy.ui.modules.grade
import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.repositories.grade.GradeRepository 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.preferences.PreferencesRepository
import io.github.wulkanowy.data.repositories.semester.SemesterRepository
import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.calcAverage import io.github.wulkanowy.utils.calcAverage
import io.github.wulkanowy.utils.changeModifier import io.github.wulkanowy.utils.changeModifier
import io.reactivex.Maybe
import io.reactivex.Single import io.reactivex.Single
import javax.inject.Inject import javax.inject.Inject
class GradeAverageProvider @Inject constructor( class GradeAverageProvider @Inject constructor(
private val preferencesRepository: PreferencesRepository, private val semesterRepository: SemesterRepository,
private val gradeRepository: GradeRepository, private val gradeRepository: GradeRepository,
private val gradeSummaryRepository: GradeSummaryRepository private val preferencesRepository: PreferencesRepository
) { ) {
private val plusModifier = preferencesRepository.gradePlusModifier private val plusModifier = preferencesRepository.gradePlusModifier
private val minusModifier = preferencesRepository.gradeMinusModifier private val minusModifier = preferencesRepository.gradeMinusModifier
fun getGradeAverage(student: Student, semesters: List<Semester>, selectedSemesterId: Int, forceRefresh: Boolean): Single<List<Triple<String, Double, String>>> { fun getGradesDetailsWithAverage(student: Student, semesterId: Int, forceRefresh: Boolean = false): Single<List<GradeDetailsWithAverage>> {
return when (preferencesRepository.gradeAverageMode) { return semesterRepository.getSemesters(student).flatMap { semesters ->
"all_year" -> getAllYearAverage(student, semesters, selectedSemesterId, forceRefresh) when (preferencesRepository.gradeAverageMode) {
"only_one_semester" -> getOnlyOneSemesterAverage(student, semesters, selectedSemesterId, forceRefresh) "only_one_semester" -> getSemesterDetailsWithAverage(student, semesters.single { it.semesterId == semesterId }, forceRefresh)
"all_year" -> calculateWholeYearAverage(student, semesters, semesterId, forceRefresh)
else -> throw IllegalArgumentException("Incorrect grade average mode: ${preferencesRepository.gradeAverageMode} ") else -> throw IllegalArgumentException("Incorrect grade average mode: ${preferencesRepository.gradeAverageMode} ")
} }
} }
}
private fun getAllYearAverage(student: Student, semesters: List<Semester>, semesterId: Int, forceRefresh: Boolean): Single<List<Triple<String, Double, String>>> { private fun calculateWholeYearAverage(student: Student, semesters: List<Semester>, semesterId: Int, forceRefresh: Boolean): Single<List<GradeDetailsWithAverage>> {
val selectedSemester = semesters.single { it.semesterId == semesterId } val selectedSemester = semesters.single { it.semesterId == semesterId }
val firstSemester = semesters.single { it.diaryId == selectedSemester.diaryId && it.semesterName == 1 } val firstSemester = semesters.single { it.diaryId == selectedSemester.diaryId && it.semesterName == 1 }
return getAverageFromGradeSummary(student, selectedSemester, forceRefresh) return getSemesterDetailsWithAverage(student, selectedSemester, forceRefresh).flatMap { selectedDetails ->
.switchIfEmpty(gradeRepository.getGrades(student, selectedSemester, forceRefresh) val isAnyAverage = selectedDetails.any { it.average != .0 }
.flatMap { firstGrades ->
if (selectedSemester == firstSemester) Single.just(firstGrades) if (selectedSemester != firstSemester) {
else { getSemesterDetailsWithAverage(student, firstSemester, forceRefresh).map { secondDetails ->
gradeRepository.getGrades(student, firstSemester) selectedDetails.map { selected ->
.map { secondGrades -> secondGrades + firstGrades } val second = secondDetails.singleOrNull { it.subject == selected.subject }
selected.copy(
average = if (!isAnyAverage || preferencesRepository.gradeAverageForceCalc) {
(selected.grades + second?.grades.orEmpty()).calcAverage()
} else (selected.average + (second?.average ?: selected.average)) / 2
)
}
}
} else Single.just(selectedDetails)
} }
}.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(), "") }
})
} }
private fun getOnlyOneSemesterAverage(student: Student, semesters: List<Semester>, semesterId: Int, forceRefresh: Boolean): Single<List<Triple<String, Double, String>>> { private fun getSemesterDetailsWithAverage(student: Student, semester: Semester, forceRefresh: Boolean): Single<List<GradeDetailsWithAverage>> {
val selectedSemester = semesters.single { it.semesterId == semesterId } return gradeRepository.getGrades(student, semester, forceRefresh).map { (details, summaries) ->
val isAnyAverage = summaries.any { it.average != .0 }
val allGrades = details.groupBy { it.subject }
return getAverageFromGradeSummary(student, selectedSemester, forceRefresh) summaries.map { summary ->
.switchIfEmpty(gradeRepository.getGrades(student, selectedSemester, forceRefresh) val grades = allGrades[summary.subject].orEmpty()
.map { grades -> GradeDetailsWithAverage(
grades.map { if (student.loginMode == Sdk.Mode.SCRAPPER.name) it.changeModifier(plusModifier, minusModifier) else it } subject = summary.subject,
.groupBy { it.subject } average = if (!isAnyAverage || preferencesRepository.gradeAverageForceCalc) {
.map { Triple(it.key, it.value.calcAverage(), "") } grades.map {
}) if (student.loginMode == Sdk.Mode.SCRAPPER.name) it.changeModifier(plusModifier, minusModifier)
else it
}.calcAverage()
} else summary.average,
points = summary.pointsSum,
summary = summary,
grades = grades
)
}
} }
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 }
} }
} }

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

View File

@ -0,0 +1,176 @@
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 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 javax.inject.Inject
class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter<RecyclerView.ViewHolder>() {
private var headers = mutableListOf<GradeDetailsItem>()
private var items = mutableListOf<GradeDetailsItem>()
private var expandedPosition = RecyclerView.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 = RecyclerView.NO_POSITION
}
fun updateDetailsItem(position: Int, grade: Grade) {
items[position] = GradeDetailsItem(grade, ViewType.ITEM)
notifyItemChanged(position)
}
fun getHeaderItem(subject: String): GradeDetailsItem {
return headers.single { (it.value as GradeDetailsHeader).subject == subject }
}
fun updateHeaderItem(item: GradeDetailsItem) {
headers[headers.indexOf(item)] = item
items[items.indexOf(item)] = item
notifyItemChanged(items.indexOf(item))
}
fun collapseAll() {
if (expandedPosition != -1) {
refreshList(headers)
expandedPosition = RecyclerView.NO_POSITION
}
}
@Synchronized
private fun refreshList(newItems: List<GradeDetailsItem>) {
val diffCallback = GradeDetailsDiffUtil(items, newItems)
val diffResult = DiffUtil.calculateDiff(diffCallback)
items = newItems.toMutableList()
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(
binding = holder.binding,
header = items[position].value as GradeDetailsHeader,
headerPosition = headers.indexOf(items[position]),
adapterPosition = position
)
is ItemViewHolder -> bindItemViewHolder(
binding = holder.binding,
grade = items[position].value as Grade,
position = position
)
}
}
private fun bindHeaderViewHolder(binding: HeaderGradeDetailsBinding, header: GradeDetailsHeader, headerPosition: Int, adapterPosition: Int) {
with(binding) {
gradeHeaderDivider.visibility = if (adapterPosition == 0) View.GONE else View.VISIBLE
gradeHeaderSubject.apply {
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 != RecyclerView.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(binding: ItemGradeDetailsBinding, grade: Grade, position: Int) {
with(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 { onClickListener(grade, position) }
}
}
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 androidx.fragment.app.DialogFragment
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Grade import io.github.wulkanowy.data.db.entities.Grade
import io.github.wulkanowy.databinding.DialogGradeBinding
import io.github.wulkanowy.utils.colorStringId import io.github.wulkanowy.utils.colorStringId
import io.github.wulkanowy.utils.getBackgroundColor import io.github.wulkanowy.utils.getBackgroundColor
import io.github.wulkanowy.utils.getGradeColor import io.github.wulkanowy.utils.getGradeColor
import io.github.wulkanowy.utils.lifecycleAwareVariable
import io.github.wulkanowy.utils.toFormattedString import io.github.wulkanowy.utils.toFormattedString
import kotlinx.android.synthetic.main.dialog_grade.*
class GradeDetailsDialog : DialogFragment() { class GradeDetailsDialog : DialogFragment() {
private var binding: DialogGradeBinding by lifecycleAwareVariable()
private lateinit var grade: Grade private lateinit var grade: Grade
private lateinit var colorScheme: String private lateinit var colorScheme: String
@ -44,12 +47,13 @@ class GradeDetailsDialog : DialogFragment() {
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { 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?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onActivityCreated(savedInstanceState)
with(binding) {
gradeDialogSubject.text = grade.subject gradeDialogSubject.text = grade.subject
gradeDialogColorAndWeightValue.run { gradeDialogColorAndWeightValue.run {
@ -87,4 +91,5 @@ class GradeDetailsDialog : DialogFragment() {
gradeDialogClose.setOnClickListener { dismiss() } gradeDialogClose.setOnClickListener { dismiss() }
} }
}
} }

View File

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

View File

@ -1,94 +0,0 @@
package io.github.wulkanowy.ui.modules.grade.details
import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractExpandableItem
import eu.davidea.flexibleadapter.items.IFlexible
import eu.davidea.viewholders.ExpandableViewHolder
import io.github.wulkanowy.R
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.header_grade_details.*
class GradeDetailsHeader(
private val subject: String,
private val number: String,
private val average: String,
private val pointsSum: String,
var newGrades: Int,
private val isExpandable: Boolean
) : AbstractExpandableItem<GradeDetailsHeader.ViewHolder, GradeDetailsItem>() {
init {
isExpanded = !isExpandable
}
override fun getLayoutRes() = R.layout.header_grade_details
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 {
gradeHeaderSubject.apply {
text = subject
maxLines = if (isExpanded) 2 else 1
}
gradeHeaderAverage.text = average
gradeHeaderPointsSum.text = pointsSum
gradeHeaderPointsSum.visibility = if (pointsSum.isNotEmpty()) VISIBLE else GONE
gradeHeaderNumber.text = number
gradeHeaderNote.visibility = if (newGrades > 0) VISIBLE else GONE
if (newGrades > 0) gradeHeaderNote.text = newGrades.toString(10)
gradeHeaderContainer.isEnabled = isExpandable
isViewExpandable = isExpandable
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as GradeDetailsHeader
if (subject != other.subject) return false
if (number != other.number) return false
if (average != other.average) return false
if (isExpandable != other.isExpandable) return false
return true
}
override fun hashCode(): Int {
var result = subject.hashCode()
result = 31 * result + number.hashCode()
result = 31 * result + average.hashCode()
result = 31 * result + isExpandable.hashCode()
return result
}
class ViewHolder(view: View?, adapter: FlexibleAdapter<IFlexible<*>>?) :
ExpandableViewHolder(view, adapter), LayoutContainer {
var isViewExpandable = true
init {
contentView.setOnClickListener(this)
}
override val containerView: View
get() = contentView
override fun isViewCollapsibleOnClick() = isViewExpandable
override fun isViewExpandableOnClick() = isViewExpandable
override fun onClick(view: View?) {
super.onClick(view)
mAdapter.getItem(adapterPosition)?.let { mAdapter.updateItem(it) }
}
}
}

View File

@ -1,38 +0,0 @@
package io.github.wulkanowy.ui.modules.grade.details
import android.content.Context
import android.graphics.Canvas
import androidx.recyclerview.widget.RecyclerView
import eu.davidea.flexibleadapter.common.FlexibleItemDecoration
class GradeDetailsHeaderItemDecoration(context: Context) : FlexibleItemDecoration(context) {
override fun drawVertical(canvas: Canvas, parent: RecyclerView) {
canvas.save()
val left: Int
val right: Int
if (parent.clipToPadding) {
left = parent.paddingLeft
right = parent.width - parent.paddingRight
canvas.clipRect(left, parent.paddingTop, right,
parent.height - parent.paddingBottom)
} else {
left = 0
right = parent.width
}
val itemCount = parent.childCount
for (i in 1 until itemCount) {
val child = parent.getChildAt(i)
val viewHolder = parent.getChildViewHolder(child)
if (shouldDrawDivider(viewHolder)) {
parent.getDecoratedBoundsWithMargins(child, mBounds)
val bottom = mBounds.top + Math.round(child.translationY)
val top = bottom - mDivider.intrinsicHeight
mDivider.setBounds(left, top, right, bottom)
mDivider.draw(canvas)
}
}
canvas.restore()
}
}

View File

@ -1,74 +1,19 @@
package io.github.wulkanowy.ui.modules.grade.details package io.github.wulkanowy.ui.modules.grade.details
import android.annotation.SuppressLint enum class ViewType(val id: Int) {
import android.view.View HEADER(1),
import android.view.View.GONE ITEM(2)
import android.view.View.VISIBLE
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.Grade
import io.github.wulkanowy.utils.toFormattedString
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.item_grade_details.*
class GradeDetailsItem(
val grade: Grade,
private val valueBgColor: Int,
private val weightString: String,
private val noDescriptionString: String
) : AbstractFlexibleItem<GradeDetailsItem.ViewHolder>() {
override fun getLayoutRes() = R.layout.item_grade_details
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<*>>): ViewHolder {
return ViewHolder(view, adapter)
}
@SuppressLint("SetTextI18n")
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<*>>, holder: ViewHolder, position: Int, payloads: MutableList<Any>?) {
holder.run {
gradeItemValue.run {
text = grade.entry
setBackgroundResource(valueBgColor)
}
gradeItemDescription.text = when {
grade.description.isNotBlank() -> grade.description
grade.gradeSymbol.isNotBlank() -> grade.gradeSymbol
else -> noDescriptionString
}
gradeItemDate.text = grade.date.toFormattedString()
gradeItemWeight.text = "$weightString: ${grade.weight}"
gradeItemNote.visibility = if (!grade.isRead) VISIBLE else GONE
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as GradeDetailsItem
if (grade != other.grade) return false
if (grade.id != other.grade.id) return false
if (weightString != other.weightString) return false
if (valueBgColor != other.valueBgColor) return false
return true
}
override fun hashCode(): Int {
var result = grade.hashCode()
result = 31 * result + grade.id.toInt()
result = 31 * result + weightString.hashCode()
result = 31 * result + valueBgColor
return result
}
class ViewHolder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter), LayoutContainer {
override val containerView: View
get() = contentView
}
} }
data class GradeDetailsItem(
val value: Any,
val viewType: ViewType
)
data class GradeDetailsHeader(
val subject: String,
val average: Double?,
val pointsSum: String?,
var newGrades: Int,
val grades: List<GradeDetailsItem>
)

View File

@ -1,6 +1,5 @@
package io.github.wulkanowy.ui.modules.grade.details package io.github.wulkanowy.ui.modules.grade.details
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import io.github.wulkanowy.data.db.entities.Grade import io.github.wulkanowy.data.db.entities.Grade
import io.github.wulkanowy.data.repositories.grade.GradeRepository import io.github.wulkanowy.data.repositories.grade.GradeRepository
import io.github.wulkanowy.data.repositories.preferences.PreferencesRepository import io.github.wulkanowy.data.repositories.preferences.PreferencesRepository
@ -9,9 +8,9 @@ import io.github.wulkanowy.data.repositories.student.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.ui.modules.grade.GradeAverageProvider import io.github.wulkanowy.ui.modules.grade.GradeAverageProvider
import io.github.wulkanowy.ui.modules.grade.GradeDetailsWithAverage
import io.github.wulkanowy.utils.FirebaseAnalyticsHelper import io.github.wulkanowy.utils.FirebaseAnalyticsHelper
import io.github.wulkanowy.utils.SchedulersProvider import io.github.wulkanowy.utils.SchedulersProvider
import io.github.wulkanowy.utils.getBackgroundColor
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -43,24 +42,20 @@ class GradeDetailsPresenter @Inject constructor(
loadData(semesterId, forceRefresh) loadData(semesterId, forceRefresh)
} }
fun onGradeItemSelected(item: AbstractFlexibleItem<*>?) { fun onGradeItemSelected(grade: Grade, position: Int) {
if (item is GradeDetailsItem) { Timber.i("Select grade item ${grade.id}")
Timber.i("Select grade item ${item.grade.id}")
view?.apply { view?.apply {
showGradeDialog(item.grade, preferencesRepository.gradeColorTheme) showGradeDialog(grade, preferencesRepository.gradeColorTheme)
if (!item.grade.isRead) { if (!grade.isRead) {
item.grade.isRead = true grade.isRead = true
updateItem(item) updateItem(grade, position)
getHeaderOfItem(item)?.let { header -> getHeaderOfItem(grade.subject).let { header ->
if (header is GradeDetailsHeader) { (header.value as GradeDetailsHeader).newGrades--
header.newGrades-- updateHeaderItem(header)
updateItem(header)
}
} }
newGradesAmount-- newGradesAmount--
updateMarkAsDoneButton() updateMarkAsDoneButton()
updateGrade(item.grade) updateGrade(grade)
}
} }
} }
} }
@ -132,16 +127,7 @@ class GradeDetailsPresenter @Inject constructor(
private fun loadData(semesterId: Int, forceRefresh: Boolean) { private fun loadData(semesterId: Int, forceRefresh: Boolean) {
Timber.i("Loading grade details data started") Timber.i("Loading grade details data started")
disposable.add(studentRepository.getCurrentStudent() disposable.add(studentRepository.getCurrentStudent()
.flatMap { semesterRepository.getSemesters(it).map { semester -> it to semester } } .flatMap { averageProvider.getGradesDetailsWithAverage(it, semesterId, forceRefresh) }
.flatMap { (student, semesters) ->
averageProvider.getGradeAverage(student, semesters, semesterId, forceRefresh)
.flatMap { averages ->
gradeRepository.getGrades(student, semesters.first { it.semesterId == semesterId }, forceRefresh)
.map { it.sortedByDescending { grade -> grade.date } }
.map { it.groupBy { grade -> grade.subject }.toSortedMap() }
.map { createGradeItems(it, averages) }
}
}
.subscribeOn(schedulers.backgroundThread) .subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread) .observeOn(schedulers.mainThread)
.doFinally { .doFinally {
@ -152,17 +138,21 @@ class GradeDetailsPresenter @Inject constructor(
notifyParentDataLoaded(semesterId) notifyParentDataLoaded(semesterId)
} }
} }
.subscribe({ .subscribe({ grades ->
Timber.i("Loading grade details result: Success") Timber.i("Loading grade details result: Success")
newGradesAmount = it.sumBy { gradeDetailsHeader -> gradeDetailsHeader.newGrades } newGradesAmount = grades.sumBy { it.grades.sumBy { grade -> if (!grade.isRead) 1 else 0 } }
updateMarkAsDoneButton() updateMarkAsDoneButton()
view?.run { view?.run {
showEmpty(it.isEmpty()) showEmpty(grades.isEmpty())
showErrorView(false) showErrorView(false)
showContent(it.isNotEmpty()) showContent(grades.isNotEmpty())
updateData(it) updateData(
data = createGradeItems(grades),
isGradeExpandable = preferencesRepository.isGradeExpandable,
gradeColorTheme = preferencesRepository.gradeColorTheme
)
} }
analytics.logEvent("load_grade_details", "items" to it.size, "force_refresh" to forceRefresh) analytics.logEvent("load_grade_details", "items" to grades.size, "force_refresh" to forceRefresh)
}) { }) {
Timber.i("Loading grade details result: An exception occurred") Timber.i("Loading grade details result: An exception occurred")
errorHandler.dispatch(it) errorHandler.dispatch(it)
@ -180,40 +170,20 @@ class GradeDetailsPresenter @Inject constructor(
} }
} }
private fun createGradeItems(items: Map<String, List<Grade>>, averages: List<Triple<String, Double, String>>): List<GradeDetailsHeader> { private fun createGradeItems(items: List<GradeDetailsWithAverage>): List<GradeDetailsItem> {
val isGradeExpandable = preferencesRepository.isGradeExpandable return items.filter { it.grades.isNotEmpty() }.map { (subject, average, points, _, grades) ->
val gradeColorTheme = preferencesRepository.gradeColorTheme val subItems = grades.map {
GradeDetailsItem(it, ViewType.ITEM)
val noDescriptionString = view?.noDescriptionString.orEmpty()
val weightString = view?.weightString.orEmpty()
val pointsSumString = view?.pointsSumString.orEmpty()
return items.map { subject ->
GradeDetailsHeader(
subject = subject.key,
average = formatAverage(averages.singleOrNull { subject.key == it.first }?.second),
pointsSum = averages.singleOrNull { subject.key == it.first }?.takeIf { it.third.isNotEmpty() }?.let { pointsSumString.format(it.third) }.orEmpty(),
number = view?.getGradeNumberString(subject.value.size).orEmpty(),
newGrades = subject.value.filter { grade -> !grade.isRead }.size,
isExpandable = isGradeExpandable
).apply {
subItems = subject.value.map { item ->
GradeDetailsItem(
grade = item,
valueBgColor = item.getBackgroundColor(gradeColorTheme),
weightString = weightString,
noDescriptionString = noDescriptionString
)
}
}
}
} }
private fun formatAverage(average: Double?): String { listOf(GradeDetailsItem(GradeDetailsHeader(
return view?.run { subject = subject,
if (average == null || average == .0) emptyAverageString average = average,
else averageString.format(average) pointsSum = points,
}.orEmpty() newGrades = grades.filter { grade -> !grade.isRead }.size,
grades = subItems
), ViewType.HEADER)) + if (preferencesRepository.isGradeExpandable) emptyList() else subItems
}.flatten()
} }
private fun updateGrade(grade: Grade) { private fun updateGrade(grade: Grade) {
@ -221,8 +191,9 @@ class GradeDetailsPresenter @Inject constructor(
disposable.add(gradeRepository.updateGrade(grade) disposable.add(gradeRepository.updateGrade(grade)
.subscribeOn(schedulers.backgroundThread) .subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread) .observeOn(schedulers.mainThread)
.subscribe({ Timber.i("Update grade result: Success") }) .subscribe({
{ error -> Timber.i("Update grade result: Success")
}) { error ->
Timber.i("Update grade result: An exception occurred") Timber.i("Update grade result: An exception occurred")
errorHandler.dispatch(error) errorHandler.dispatch(error)
}) })

View File

@ -1,8 +1,5 @@
package io.github.wulkanowy.ui.modules.grade.details package io.github.wulkanowy.ui.modules.grade.details
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import eu.davidea.flexibleadapter.items.IExpandable
import eu.davidea.flexibleadapter.items.IFlexible
import io.github.wulkanowy.data.db.entities.Grade import io.github.wulkanowy.data.db.entities.Grade
import io.github.wulkanowy.ui.base.BaseView import io.github.wulkanowy.ui.base.BaseView
@ -10,21 +7,13 @@ interface GradeDetailsView : BaseView {
val isViewEmpty: Boolean val isViewEmpty: Boolean
val emptyAverageString: String
val averageString: String
val pointsSumString: String
val weightString: String
val noDescriptionString: String
fun initView() fun initView()
fun updateData(data: List<GradeDetailsHeader>) fun updateData(data: List<GradeDetailsItem>, isGradeExpandable: Boolean, gradeColorTheme: String)
fun updateItem(item: AbstractFlexibleItem<*>) fun updateItem(item: Grade, position: Int)
fun updateHeaderItem(item: GradeDetailsItem)
fun clearView() fun clearView()
@ -54,7 +43,5 @@ interface GradeDetailsView : BaseView {
fun enableMarkAsDoneButton(enable: Boolean) fun enableMarkAsDoneButton(enable: Boolean)
fun getGradeNumberString(number: Int): String fun getHeaderOfItem(subject: String): GradeDetailsItem
fun getHeaderOfItem(item: AbstractFlexibleItem<*>): IExpandable<*, out IFlexible<*>>?
} }

View File

@ -2,7 +2,6 @@ package io.github.wulkanowy.ui.modules.grade.statistics
import android.graphics.Color import android.graphics.Color
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.View.GONE import android.view.View.GONE
import android.view.View.VISIBLE import android.view.View.VISIBLE
import android.view.ViewGroup import android.view.ViewGroup
@ -21,9 +20,9 @@ import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.GradePointsStatistics import io.github.wulkanowy.data.db.entities.GradePointsStatistics
import io.github.wulkanowy.data.db.entities.GradeStatistics import io.github.wulkanowy.data.db.entities.GradeStatistics
import io.github.wulkanowy.data.pojos.GradeStatisticsItem import io.github.wulkanowy.data.pojos.GradeStatisticsItem
import io.github.wulkanowy.databinding.ItemGradeStatisticsBarBinding
import io.github.wulkanowy.databinding.ItemGradeStatisticsPieBinding
import io.github.wulkanowy.utils.getThemeAttrColor import io.github.wulkanowy.utils.getThemeAttrColor
import kotlinx.android.synthetic.main.item_grade_statistics_bar.view.*
import kotlinx.android.synthetic.main.item_grade_statistics_pie.view.*
import javax.inject.Inject import javax.inject.Inject
class GradeStatisticsAdapter @Inject constructor() : class GradeStatisticsAdapter @Inject constructor() :
@ -33,6 +32,8 @@ class GradeStatisticsAdapter @Inject constructor() :
var theme: String = "vulcan" var theme: String = "vulcan"
var showAllSubjectsOnList: Boolean = false
private val vulcanGradeColors = listOf( private val vulcanGradeColors = listOf(
6 to R.color.grade_vulcan_six, 6 to R.color.grade_vulcan_six,
5 to R.color.grade_vulcan_five, 5 to R.color.grade_vulcan_five,
@ -60,34 +61,30 @@ class GradeStatisticsAdapter @Inject constructor() :
"6, 6-", "5, 5-, 5+", "4, 4-, 4+", "3, 3-, 3+", "2, 2-, 2+", "1, 1+" "6, 6-", "5, 5-, 5+", "4, 4-, 4+", "3, 3-, 3+", "2, 2-, 2+", "1, 1+"
) )
override fun getItemCount() = items.size override fun getItemCount() = if (showAllSubjectsOnList) items.size else (if (items.isEmpty()) 0 else 1)
override fun getItemViewType(position: Int): Int { override fun getItemViewType(position: Int) = items[position].type.id
return when (items[position].type) {
ViewType.SEMESTER, ViewType.PARTIAL -> R.layout.item_grade_statistics_pie
ViewType.POINTS -> R.layout.item_grade_statistics_bar
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val viewHolder = LayoutInflater.from(parent.context).inflate(viewType, parent, false) val inflater = LayoutInflater.from(parent.context)
return when (viewType) { return when (viewType) {
R.layout.item_grade_statistics_bar -> GradeStatisticsBar(viewHolder) ViewType.PARTIAL.id, ViewType.SEMESTER.id -> PieViewHolder(ItemGradeStatisticsPieBinding.inflate(inflater, parent, false))
else -> GradeStatisticsPie(viewHolder) ViewType.POINTS.id -> BarViewHolder(ItemGradeStatisticsBarBinding.inflate(inflater, parent, false))
else -> throw IllegalStateException()
} }
} }
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) { when (holder) {
is GradeStatisticsPie -> bindPieChart(holder, items[position].partial) is PieViewHolder -> bindPieChart(holder, items[position].partial)
is GradeStatisticsBar -> bindBarChart(holder, items[position].points!!) is BarViewHolder -> bindBarChart(holder, items[position].points!!)
} }
} }
private fun bindPieChart(holder: GradeStatisticsPie, partials: List<GradeStatistics>) { private fun bindPieChart(holder: PieViewHolder, partials: List<GradeStatistics>) {
with(holder.view.gradeStatisticsPieTitle) { with(holder.binding.gradeStatisticsPieTitle) {
text = partials.firstOrNull()?.subject text = partials.firstOrNull()?.subject
visibility = if (items.size == 1) GONE else VISIBLE visibility = if (items.size == 1 || !showAllSubjectsOnList) GONE else VISIBLE
} }
val gradeColors = when (theme) { val gradeColors = when (theme) {
@ -105,10 +102,10 @@ class GradeStatisticsAdapter @Inject constructor() :
valueTextColor = Color.WHITE valueTextColor = Color.WHITE
setColors(partials.map { setColors(partials.map {
gradeColors.single { color -> color.first == it.grade }.second gradeColors.single { color -> color.first == it.grade }.second
}.toIntArray(), holder.view.context) }.toIntArray(), holder.binding.root.context)
} }
with(holder.view.gradeStatisticsPie) { with(holder.binding.gradeStatisticsPie) {
setTouchEnabled(false) setTouchEnabled(false)
if (partials.size == 1) animateXY(1000, 1000) if (partials.size == 1) animateXY(1000, 1000)
data = PieData(dataset).apply { data = PieData(dataset).apply {
@ -140,8 +137,8 @@ class GradeStatisticsAdapter @Inject constructor() :
} }
} }
private fun bindBarChart(holder: GradeStatisticsBar, points: GradePointsStatistics) { private fun bindBarChart(holder: BarViewHolder, points: GradePointsStatistics) {
with(holder.view.gradeStatisticsBarTitle) { with(holder.binding.gradeStatisticsBarTitle) {
text = points.subject text = points.subject
visibility = if (items.size == 1) GONE else VISIBLE visibility = if (items.size == 1) GONE else VISIBLE
} }
@ -153,14 +150,14 @@ class GradeStatisticsAdapter @Inject constructor() :
with(dataset) { with(dataset) {
valueTextSize = 12f valueTextSize = 12f
valueTextColor = holder.view.context.getThemeAttrColor(android.R.attr.textColorPrimary) valueTextColor = holder.binding.root.context.getThemeAttrColor(android.R.attr.textColorPrimary)
valueFormatter = object : ValueFormatter() { valueFormatter = object : ValueFormatter() {
override fun getBarLabel(barEntry: BarEntry) = "${barEntry.y}%" override fun getBarLabel(barEntry: BarEntry) = "${barEntry.y}%"
} }
colors = gradePointsColors colors = gradePointsColors
} }
with(holder.view.gradeStatisticsBar) { with(holder.binding.gradeStatisticsBar) {
setTouchEnabled(false) setTouchEnabled(false)
if (items.size == 1) animateXY(1000, 1000) if (items.size == 1) animateXY(1000, 1000)
data = BarData(dataset).apply { data = BarData(dataset).apply {
@ -183,7 +180,7 @@ class GradeStatisticsAdapter @Inject constructor() :
description.isEnabled = false description.isEnabled = false
holder.view.context.getThemeAttrColor(android.R.attr.textColorPrimary).let { holder.binding.root.context.getThemeAttrColor(android.R.attr.textColorPrimary).let {
axisLeft.textColor = it axisLeft.textColor = it
axisRight.textColor = it axisRight.textColor = it
} }
@ -203,7 +200,9 @@ class GradeStatisticsAdapter @Inject constructor() :
} }
} }
class GradeStatisticsPie(val view: View) : RecyclerView.ViewHolder(view) private class PieViewHolder(val binding: ItemGradeStatisticsPieBinding) :
RecyclerView.ViewHolder(binding.root)
class GradeStatisticsBar(val view: View) : RecyclerView.ViewHolder(view) private class BarViewHolder(val binding: ItemGradeStatisticsBarBinding) :
RecyclerView.ViewHolder(binding.root)
} }

View File

@ -1,23 +1,23 @@
package io.github.wulkanowy.ui.modules.grade.statistics package io.github.wulkanowy.ui.modules.grade.statistics
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.TextView import android.widget.TextView
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.pojos.GradeStatisticsItem import io.github.wulkanowy.data.pojos.GradeStatisticsItem
import io.github.wulkanowy.databinding.FragmentGradeStatisticsBinding
import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.grade.GradeFragment import io.github.wulkanowy.ui.modules.grade.GradeFragment
import io.github.wulkanowy.ui.modules.grade.GradeView import io.github.wulkanowy.ui.modules.grade.GradeView
import io.github.wulkanowy.utils.dpToPx import io.github.wulkanowy.utils.dpToPx
import io.github.wulkanowy.utils.setOnItemSelectedListener import io.github.wulkanowy.utils.setOnItemSelectedListener
import kotlinx.android.synthetic.main.fragment_grade_statistics.*
import javax.inject.Inject import javax.inject.Inject
class GradeStatisticsFragment : BaseFragment(), GradeStatisticsView, GradeView.GradeChildView { class GradeStatisticsFragment :
BaseFragment<FragmentGradeStatisticsBinding>(R.layout.fragment_grade_statistics),
GradeStatisticsView, GradeView.GradeChildView {
@Inject @Inject
lateinit var presenter: GradeStatisticsPresenter lateinit var presenter: GradeStatisticsPresenter
@ -36,24 +36,21 @@ class GradeStatisticsFragment : BaseFragment(), GradeStatisticsView, GradeView.G
override val isViewEmpty get() = statisticsAdapter.items.isEmpty() override val isViewEmpty get() = statisticsAdapter.items.isEmpty()
override val currentType override val currentType
get() = when (gradeStatisticsTypeSwitch.checkedRadioButtonId) { get() = when (binding.gradeStatisticsTypeSwitch.checkedRadioButtonId) {
R.id.gradeStatisticsTypeSemester -> ViewType.SEMESTER R.id.gradeStatisticsTypeSemester -> ViewType.SEMESTER
R.id.gradeStatisticsTypePartial -> ViewType.PARTIAL R.id.gradeStatisticsTypePartial -> ViewType.PARTIAL
else -> ViewType.POINTS else -> ViewType.POINTS
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
return inflater.inflate(R.layout.fragment_grade_statistics, container, false) super.onViewCreated(view, savedInstanceState)
} binding = FragmentGradeStatisticsBinding.bind(view)
messageContainer = binding.gradeStatisticsSwipe
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
messageContainer = gradeStatisticsSwipe
presenter.onAttachView(this, savedInstanceState?.getSerializable(SAVED_CHART_TYPE) as? ViewType) presenter.onAttachView(this, savedInstanceState?.getSerializable(SAVED_CHART_TYPE) as? ViewType)
} }
override fun initView() { override fun initView() {
with(gradeStatisticsRecycler) { with(binding.gradeStatisticsRecycler) {
layoutManager = LinearLayoutManager(requireContext()) layoutManager = LinearLayoutManager(requireContext())
adapter = statisticsAdapter adapter = statisticsAdapter
} }
@ -61,17 +58,19 @@ class GradeStatisticsFragment : BaseFragment(), GradeStatisticsView, GradeView.G
subjectsAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, mutableListOf()) subjectsAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, mutableListOf())
subjectsAdapter.setDropDownViewResource(R.layout.item_attendance_summary_subject) subjectsAdapter.setDropDownViewResource(R.layout.item_attendance_summary_subject)
with(gradeStatisticsSubjects) { with(binding.gradeStatisticsSubjects) {
adapter = subjectsAdapter adapter = subjectsAdapter
setOnItemSelectedListener<TextView> { presenter.onSubjectSelected(it?.text?.toString()) } setOnItemSelectedListener<TextView> { presenter.onSubjectSelected(it?.text?.toString()) }
} }
with(binding) {
gradeStatisticsSubjectsContainer.setElevationCompat(requireContext().dpToPx(1f)) gradeStatisticsSubjectsContainer.setElevationCompat(requireContext().dpToPx(1f))
gradeStatisticsSwipe.setOnRefreshListener(presenter::onSwipeRefresh) gradeStatisticsSwipe.setOnRefreshListener(presenter::onSwipeRefresh)
gradeStatisticsErrorRetry.setOnClickListener { presenter.onRetry() } gradeStatisticsErrorRetry.setOnClickListener { presenter.onRetry() }
gradeStatisticsErrorDetails.setOnClickListener { presenter.onDetailsClick() } gradeStatisticsErrorDetails.setOnClickListener { presenter.onDetailsClick() }
} }
}
override fun updateSubjects(data: ArrayList<String>) { override fun updateSubjects(data: ArrayList<String>) {
with(subjectsAdapter) { with(subjectsAdapter) {
@ -81,15 +80,17 @@ class GradeStatisticsFragment : BaseFragment(), GradeStatisticsView, GradeView.G
} }
} }
override fun updateData(items: List<GradeStatisticsItem>, theme: String) { override fun updateData(items: List<GradeStatisticsItem>, theme: String, showAllSubjectsOnStatisticsList: Boolean) {
statisticsAdapter.theme = theme with(statisticsAdapter) {
statisticsAdapter.items = items this.showAllSubjectsOnList = showAllSubjectsOnStatisticsList
statisticsAdapter.notifyDataSetChanged() this.theme = theme
this.items = items
notifyDataSetChanged()
}
} }
override fun showSubjects(show: Boolean) { override fun showSubjects(show: Boolean) {
gradeStatisticsSubjectsContainer.visibility = if (show) View.VISIBLE else View.INVISIBLE binding.gradeStatisticsSubjectsContainer.visibility = if (show) View.VISIBLE else View.GONE
gradeStatisticsTypeSwitch.visibility = if (show) View.VISIBLE else View.INVISIBLE
} }
override fun clearView() { override fun clearView() {
@ -97,35 +98,35 @@ class GradeStatisticsFragment : BaseFragment(), GradeStatisticsView, GradeView.G
} }
override fun resetView() { override fun resetView() {
gradeStatisticsScroll.scrollTo(0, 0) binding.gradeStatisticsScroll.scrollTo(0, 0)
} }
override fun showContent(show: Boolean) { override fun showContent(show: Boolean) {
gradeStatisticsRecycler.visibility = if (show) View.VISIBLE else View.GONE binding.gradeStatisticsRecycler.visibility = if (show) View.VISIBLE else View.GONE
} }
override fun showEmpty(show: Boolean) { override fun showEmpty(show: Boolean) {
gradeStatisticsEmpty.visibility = if (show) View.VISIBLE else View.INVISIBLE binding.gradeStatisticsEmpty.visibility = if (show) View.VISIBLE else View.INVISIBLE
} }
override fun showErrorView(show: Boolean) { override fun showErrorView(show: Boolean) {
gradeStatisticsError.visibility = if (show) View.VISIBLE else View.GONE binding.gradeStatisticsError.visibility = if (show) View.VISIBLE else View.GONE
} }
override fun setErrorDetails(message: String) { override fun setErrorDetails(message: String) {
gradeStatisticsErrorMessage.text = message binding.gradeStatisticsErrorMessage.text = message
} }
override fun showProgress(show: Boolean) { override fun showProgress(show: Boolean) {
gradeStatisticsProgress.visibility = if (show) View.VISIBLE else View.GONE binding.gradeStatisticsProgress.visibility = if (show) View.VISIBLE else View.GONE
} }
override fun enableSwipe(enable: Boolean) { override fun enableSwipe(enable: Boolean) {
gradeStatisticsSwipe.isEnabled = enable binding.gradeStatisticsSwipe.isEnabled = enable
} }
override fun showRefresh(show: Boolean) { override fun showRefresh(show: Boolean) {
gradeStatisticsSwipe.isRefreshing = show binding.gradeStatisticsSwipe.isRefreshing = show
} }
override fun onParentLoadData(semesterId: Int, forceRefresh: Boolean) { override fun onParentLoadData(semesterId: Int, forceRefresh: Boolean) {
@ -150,7 +151,7 @@ class GradeStatisticsFragment : BaseFragment(), GradeStatisticsView, GradeView.G
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
gradeStatisticsTypeSwitch.setOnCheckedChangeListener { _, _ -> presenter.onTypeChange() } binding.gradeStatisticsTypeSwitch.setOnCheckedChangeListener { _, _ -> presenter.onTypeChange() }
} }
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
@ -159,7 +160,7 @@ class GradeStatisticsFragment : BaseFragment(), GradeStatisticsView, GradeView.G
} }
override fun onDestroyView() { override fun onDestroyView() {
super.onDestroyView()
presenter.onDetachView() presenter.onDetachView()
super.onDestroyView()
} }
} }

View File

@ -128,10 +128,7 @@ class GradeStatisticsPresenter @Inject constructor(
.observeOn(schedulers.mainThread) .observeOn(schedulers.mainThread)
.subscribe({ .subscribe({
Timber.i("Loading grade stats subjects result: Success") Timber.i("Loading grade stats subjects result: Success")
view?.run { view?.updateSubjects(it)
updateSubjects(it)
showSubjects(true)
}
}, { }, {
Timber.i("Loading grade stats subjects result: An exception occurred") Timber.i("Loading grade stats subjects result: An exception occurred")
errorHandler.dispatch(it) errorHandler.dispatch(it)
@ -140,22 +137,21 @@ class GradeStatisticsPresenter @Inject constructor(
} }
private fun loadDataByType(semesterId: Int, subjectName: String, type: ViewType, forceRefresh: Boolean = false) { private fun loadDataByType(semesterId: Int, subjectName: String, type: ViewType, forceRefresh: Boolean = false) {
currentSubjectName = subjectName currentSubjectName = if (preferencesRepository.showAllSubjectsOnStatisticsList) "Wszystkie" else subjectName
currentType = type currentType = type
loadData(semesterId, subjectName, type, forceRefresh)
}
private fun loadData(semesterId: Int, subjectName: String, type: ViewType, forceRefresh: Boolean) {
Timber.i("Loading grade stats data started") Timber.i("Loading grade stats data started")
disposable.add(studentRepository.getCurrentStudent() disposable.add(studentRepository.getCurrentStudent()
.flatMap { student -> .flatMap { student ->
semesterRepository.getSemesters(student).flatMap { semesters -> semesterRepository.getSemesters(student).flatMap { semesters ->
val semester = semesters.first { item -> item.semesterId == semesterId } val semester = semesters.first { item -> item.semesterId == semesterId }
with(gradeStatisticsRepository) {
when (type) { when (type) {
ViewType.SEMESTER -> gradeStatisticsRepository.getGradesStatistics(student, semester, subjectName, true, forceRefresh) ViewType.SEMESTER -> getGradesStatistics(student, semester, currentSubjectName, true, forceRefresh)
ViewType.PARTIAL -> gradeStatisticsRepository.getGradesStatistics(student, semester, subjectName, false, forceRefresh) ViewType.PARTIAL -> getGradesStatistics(student, semester, currentSubjectName, false, forceRefresh)
ViewType.POINTS -> gradeStatisticsRepository.getGradesPointsStatistics(student, semester, subjectName, forceRefresh) ViewType.POINTS -> getGradesPointsStatistics(student, semester, currentSubjectName, forceRefresh)
}
} }
} }
} }
@ -175,7 +171,8 @@ class GradeStatisticsPresenter @Inject constructor(
showEmpty(it.isEmpty()) showEmpty(it.isEmpty())
showContent(it.isNotEmpty()) showContent(it.isNotEmpty())
showErrorView(false) showErrorView(false)
updateData(it, preferencesRepository.gradeColorTheme) updateData(it, preferencesRepository.gradeColorTheme, preferencesRepository.showAllSubjectsOnStatisticsList)
showSubjects(!preferencesRepository.showAllSubjectsOnStatisticsList)
} }
analytics.logEvent("load_grade_statistics", "items" to it.size, "force_refresh" to forceRefresh) analytics.logEvent("load_grade_statistics", "items" to it.size, "force_refresh" to forceRefresh)
}) { }) {

View File

@ -13,7 +13,7 @@ interface GradeStatisticsView : BaseView {
fun updateSubjects(data: ArrayList<String>) fun updateSubjects(data: ArrayList<String>)
fun updateData(items: List<GradeStatisticsItem>, theme: String) fun updateData(items: List<GradeStatisticsItem>, theme: String, showAllSubjectsOnStatisticsList: Boolean)
fun showSubjects(show: Boolean) fun showSubjects(show: Boolean)

View File

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

View File

@ -0,0 +1,85 @@
package io.github.wulkanowy.ui.modules.grade.summary
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import io.github.wulkanowy.data.db.entities.GradeSummary
import io.github.wulkanowy.databinding.ItemGradeSummaryBinding
import io.github.wulkanowy.databinding.ScrollableHeaderGradeSummaryBinding
import io.github.wulkanowy.utils.calcAverage
import java.util.Locale
import javax.inject.Inject
class GradeSummaryAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private enum class ViewType(val id: Int) {
HEADER(1),
ITEM(2)
}
var items = emptyList<GradeSummary>()
override fun getItemCount() = items.size + 1
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(ScrollableHeaderGradeSummaryBinding.inflate(inflater, parent, false))
ViewType.ITEM.id -> ItemViewHolder(ItemGradeSummaryBinding.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, items[position - 1])
}
}
private fun bindHeaderViewHolder(binding: ScrollableHeaderGradeSummaryBinding) {
if (items.isEmpty()) return
with(binding) {
gradeSummaryScrollableHeaderFinal.text = formatAverage(items.calcAverage())
gradeSummaryScrollableHeaderCalculated.text = formatAverage(items
.filter { value -> value.average != 0.0 }
.map { values -> values.average }
.reversed() // fix average precision
.average()
)
}
}
@SuppressLint("SetTextI18n")
private fun bindItemViewHolder(binding: ItemGradeSummaryBinding, item: GradeSummary) {
with(binding) {
gradeSummaryItemTitle.text = item.subject
gradeSummaryItemPoints.text = item.pointsSum
gradeSummaryItemAverage.text = formatAverage(item.average, "")
gradeSummaryItemPredicted.text = "${item.predictedGrade} ${item.proposedPoints}".trim()
gradeSummaryItemFinal.text = "${item.finalGrade} ${item.finalPoints}".trim()
gradeSummaryItemPointsContainer.visibility = if (item.pointsSum.isBlank()) View.GONE else View.VISIBLE
}
}
private fun formatAverage(average: Double, defaultValue: String = "-- --"): String {
return if (average == 0.0) defaultValue
else String.format(Locale.FRANCE, "%.2f", average)
}
private class HeaderViewHolder(val binding: ScrollableHeaderGradeSummaryBinding) :
RecyclerView.ViewHolder(binding.root)
private class ItemViewHolder(val binding: ItemGradeSummaryBinding) :
RecyclerView.ViewHolder(binding.root)
}

View File

@ -1,36 +1,35 @@
package io.github.wulkanowy.ui.modules.grade.summary package io.github.wulkanowy.ui.modules.grade.summary
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.View.GONE import android.view.View.GONE
import android.view.View.INVISIBLE import android.view.View.INVISIBLE
import android.view.View.VISIBLE import android.view.View.VISIBLE
import android.view.ViewGroup import androidx.recyclerview.widget.LinearLayoutManager
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.R
import io.github.wulkanowy.data.db.entities.GradeSummary
import io.github.wulkanowy.databinding.FragmentGradeSummaryBinding
import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.grade.GradeFragment import io.github.wulkanowy.ui.modules.grade.GradeFragment
import io.github.wulkanowy.ui.modules.grade.GradeView import io.github.wulkanowy.ui.modules.grade.GradeView
import kotlinx.android.synthetic.main.fragment_grade_summary.*
import javax.inject.Inject import javax.inject.Inject
class GradeSummaryFragment : BaseFragment(), GradeSummaryView, GradeView.GradeChildView { class GradeSummaryFragment :
BaseFragment<FragmentGradeSummaryBinding>(R.layout.fragment_grade_summary), GradeSummaryView,
GradeView.GradeChildView {
@Inject @Inject
lateinit var presenter: GradeSummaryPresenter lateinit var presenter: GradeSummaryPresenter
@Inject @Inject
lateinit var gradeSummaryAdapter: FlexibleAdapter<AbstractFlexibleItem<*>> lateinit var gradeSummaryAdapter: GradeSummaryAdapter
companion object { companion object {
fun newInstance() = GradeSummaryFragment() fun newInstance() = GradeSummaryFragment()
} }
override val isViewEmpty override val isViewEmpty
get() = gradeSummaryAdapter.isEmpty get() = gradeSummaryAdapter.items.isEmpty()
override val predictedString override val predictedString
get() = getString(R.string.grade_summary_predicted_grade) get() = getString(R.string.grade_summary_predicted_grade)
@ -38,70 +37,69 @@ class GradeSummaryFragment : BaseFragment(), GradeSummaryView, GradeView.GradeCh
override val finalString override val finalString
get() = getString(R.string.grade_summary_final_grade) get() = getString(R.string.grade_summary_final_grade)
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
return inflater.inflate(R.layout.fragment_grade_summary, container, false) super.onViewCreated(view, savedInstanceState)
} binding = FragmentGradeSummaryBinding.bind(view)
messageContainer = binding.gradeSummaryRecycler
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
messageContainer = gradeSummaryRecycler
presenter.onAttachView(this) presenter.onAttachView(this)
} }
override fun initView() { override fun initView() {
gradeSummaryAdapter.setDisplayHeadersAtStartUp(true) with(binding.gradeSummaryRecycler) {
layoutManager = LinearLayoutManager(context)
gradeSummaryRecycler.run {
layoutManager = SmoothScrollLinearLayoutManager(context)
adapter = gradeSummaryAdapter adapter = gradeSummaryAdapter
} }
with(binding) {
gradeSummarySwipe.setOnRefreshListener { presenter.onSwipeRefresh() } gradeSummarySwipe.setOnRefreshListener { presenter.onSwipeRefresh() }
gradeSummaryErrorRetry.setOnClickListener { presenter.onRetry() } gradeSummaryErrorRetry.setOnClickListener { presenter.onRetry() }
gradeSummaryErrorDetails.setOnClickListener { presenter.onDetailsClick() } gradeSummaryErrorDetails.setOnClickListener { presenter.onDetailsClick() }
} }
}
override fun updateData(data: List<GradeSummaryItem>, header: GradeSummaryScrollableHeader) { override fun updateData(data: List<GradeSummary>) {
gradeSummaryAdapter.apply { with(gradeSummaryAdapter) {
updateDataSet(data, true) items = data
removeAllScrollableHeaders() notifyDataSetChanged()
addScrollableHeader(header)
} }
} }
override fun clearView() { override fun clearView() {
gradeSummaryAdapter.clear() with(gradeSummaryAdapter) {
items = emptyList()
notifyDataSetChanged()
}
} }
override fun resetView() { override fun resetView() {
gradeSummaryRecycler.scrollToPosition(0) binding.gradeSummaryRecycler.scrollToPosition(0)
} }
override fun showContent(show: Boolean) { override fun showContent(show: Boolean) {
gradeSummaryRecycler.visibility = if (show) VISIBLE else INVISIBLE binding.gradeSummaryRecycler.visibility = if (show) VISIBLE else INVISIBLE
} }
override fun showEmpty(show: Boolean) { override fun showEmpty(show: Boolean) {
gradeSummaryEmpty.visibility = if (show) VISIBLE else INVISIBLE binding.gradeSummaryEmpty.visibility = if (show) VISIBLE else INVISIBLE
} }
override fun showErrorView(show: Boolean) { override fun showErrorView(show: Boolean) {
gradeSummaryError.visibility = if (show) VISIBLE else INVISIBLE binding.gradeSummaryError.visibility = if (show) VISIBLE else INVISIBLE
} }
override fun setErrorDetails(message: String) { override fun setErrorDetails(message: String) {
gradeSummaryErrorMessage.text = message binding.gradeSummaryErrorMessage.text = message
} }
override fun showProgress(show: Boolean) { override fun showProgress(show: Boolean) {
gradeSummaryProgress.visibility = if (show) VISIBLE else GONE binding.gradeSummaryProgress.visibility = if (show) VISIBLE else GONE
} }
override fun enableSwipe(enable: Boolean) { override fun enableSwipe(enable: Boolean) {
gradeSummarySwipe.isEnabled = enable binding.gradeSummarySwipe.isEnabled = enable
} }
override fun showRefresh(show: Boolean) { override fun showRefresh(show: Boolean) {
gradeSummarySwipe.isRefreshing = show binding.gradeSummarySwipe.isRefreshing = show
} }
override fun onParentLoadData(semesterId: Int, forceRefresh: Boolean) { override fun onParentLoadData(semesterId: Int, forceRefresh: Boolean) {
@ -125,7 +123,7 @@ class GradeSummaryFragment : BaseFragment(), GradeSummaryView, GradeView.GradeCh
} }
override fun onDestroyView() { override fun onDestroyView() {
super.onDestroyView()
presenter.onDetachView() presenter.onDetachView()
super.onDestroyView()
} }
} }

View File

@ -1,64 +0,0 @@
package io.github.wulkanowy.ui.modules.grade.summary
import android.annotation.SuppressLint
import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
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.GradeSummary
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.item_grade_summary.*
class GradeSummaryItem(
val summary: GradeSummary,
private val average: String
) : AbstractFlexibleItem<GradeSummaryItem.ViewHolder>() {
override fun getLayoutRes() = R.layout.item_grade_summary
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<*>>): ViewHolder {
return ViewHolder(view, adapter)
}
@SuppressLint("SetTextI18n")
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<*>>, holder: ViewHolder, position: Int, payloads: MutableList<Any>?) {
holder.run {
gradeSummaryItemTitle.text = summary.subject
gradeSummaryItemPoints.text = summary.pointsSum
gradeSummaryItemAverage.text = average
gradeSummaryItemPredicted.text = "${summary.predictedGrade} ${summary.proposedPoints}".trim()
gradeSummaryItemFinal.text = "${summary.finalGrade} ${summary.finalPoints}".trim()
gradeSummaryItemPointsContainer.visibility = if (summary.pointsSum.isBlank()) GONE else VISIBLE
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as GradeSummaryItem
if (average != other.average) return false
if (summary != other.summary) return false
if (summary.id != other.summary.id) return false
return true
}
override fun hashCode(): Int {
var result = summary.hashCode()
result = 31 * result + summary.id.hashCode()
result = 31 * result + average.hashCode()
return result
}
class ViewHolder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter), LayoutContainer {
override val containerView: View
get() = contentView
}
}

View File

@ -1,26 +1,20 @@
package io.github.wulkanowy.ui.modules.grade.summary package io.github.wulkanowy.ui.modules.grade.summary
import io.github.wulkanowy.data.db.entities.GradeSummary import io.github.wulkanowy.data.db.entities.GradeSummary
import io.github.wulkanowy.data.repositories.gradessummary.GradeSummaryRepository
import io.github.wulkanowy.data.repositories.semester.SemesterRepository
import io.github.wulkanowy.data.repositories.student.StudentRepository import io.github.wulkanowy.data.repositories.student.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.ui.modules.grade.GradeAverageProvider import io.github.wulkanowy.ui.modules.grade.GradeAverageProvider
import io.github.wulkanowy.ui.modules.grade.GradeDetailsWithAverage
import io.github.wulkanowy.utils.FirebaseAnalyticsHelper import io.github.wulkanowy.utils.FirebaseAnalyticsHelper
import io.github.wulkanowy.utils.SchedulersProvider import io.github.wulkanowy.utils.SchedulersProvider
import io.github.wulkanowy.utils.calcAverage
import timber.log.Timber import timber.log.Timber
import java.lang.String.format
import java.util.Locale.FRANCE
import javax.inject.Inject import javax.inject.Inject
class GradeSummaryPresenter @Inject constructor( class GradeSummaryPresenter @Inject constructor(
schedulers: SchedulersProvider, schedulers: SchedulersProvider,
errorHandler: ErrorHandler, errorHandler: ErrorHandler,
studentRepository: StudentRepository, studentRepository: StudentRepository,
private val gradeSummaryRepository: GradeSummaryRepository,
private val semesterRepository: SemesterRepository,
private val averageProvider: GradeAverageProvider, private val averageProvider: GradeAverageProvider,
private val analytics: FirebaseAnalyticsHelper private val analytics: FirebaseAnalyticsHelper
) : BasePresenter<GradeSummaryView>(errorHandler, studentRepository, schedulers) { ) : BasePresenter<GradeSummaryView>(errorHandler, studentRepository, schedulers) {
@ -36,15 +30,8 @@ class GradeSummaryPresenter @Inject constructor(
fun onParentViewLoadData(semesterId: Int, forceRefresh: Boolean) { fun onParentViewLoadData(semesterId: Int, forceRefresh: Boolean) {
Timber.i("Loading grade summary data started") Timber.i("Loading grade summary data started")
disposable.add(studentRepository.getCurrentStudent() disposable.add(studentRepository.getCurrentStudent()
.flatMap { semesterRepository.getSemesters(it).map { semesters -> it to semesters } } .flatMap { averageProvider.getGradesDetailsWithAverage(it, semesterId, forceRefresh) }
.flatMap { (student, semesters) -> .map { createGradeSummaryItems(it) }
gradeSummaryRepository.getGradesSummary(student, semesters.first { it.semesterId == semesterId }, forceRefresh)
.map { it.sortedBy { subject -> subject.subject } }
.flatMap { gradesSummary ->
averageProvider.getGradeAverage(student, semesters, semesterId, forceRefresh)
.map { averages -> createGradeSummaryItemsAndHeader(gradesSummary, averages) }
}
}
.subscribeOn(schedulers.backgroundThread) .subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread) .observeOn(schedulers.mainThread)
.doFinally { .doFinally {
@ -54,15 +41,15 @@ class GradeSummaryPresenter @Inject constructor(
enableSwipe(true) enableSwipe(true)
notifyParentDataLoaded(semesterId) notifyParentDataLoaded(semesterId)
} }
}.subscribe({ (gradeSummaryItems, gradeSummaryHeader) -> }.subscribe({
Timber.i("Loading grade summary result: Success") Timber.i("Loading grade summary result: Success")
view?.run { view?.run {
showEmpty(gradeSummaryItems.isEmpty()) showEmpty(it.isEmpty())
showContent(gradeSummaryItems.isNotEmpty()) showContent(it.isNotEmpty())
showErrorView(false) showErrorView(false)
updateData(gradeSummaryItems, gradeSummaryHeader) updateData(it)
} }
analytics.logEvent("load_grade_summary", "items" to gradeSummaryItems.size, "force_refresh" to forceRefresh) analytics.logEvent("load_grade_summary", "items" to it.size, "force_refresh" to forceRefresh)
}) { }) {
Timber.i("Loading grade summary result: An exception occurred") Timber.i("Loading grade summary result: An exception occurred")
errorHandler.dispatch(it) errorHandler.dispatch(it)
@ -115,20 +102,9 @@ class GradeSummaryPresenter @Inject constructor(
disposable.clear() disposable.clear()
} }
private fun createGradeSummaryItemsAndHeader(gradesSummary: List<GradeSummary>, averages: List<Triple<String, Double, String>>): Pair<List<GradeSummaryItem>, GradeSummaryScrollableHeader> { private fun createGradeSummaryItems(items: List<GradeDetailsWithAverage>): List<GradeSummary> {
return averages.filter { value -> value.second != 0.0 } return items.map {
.let { filteredAverages -> it.summary.copy(average = it.average)
gradesSummary.filter { !checkEmpty(it, filteredAverages) }
.map { gradeSummary ->
GradeSummaryItem(
summary = gradeSummary,
average = formatAverage(filteredAverages.singleOrNull { gradeSummary.subject == it.first }?.second ?: .0, "")
)
}.let {
it to GradeSummaryScrollableHeader(
formatAverage(gradesSummary.calcAverage()),
formatAverage(filteredAverages.map { values -> values.second }.average()))
}
} }
} }
@ -137,9 +113,4 @@ class GradeSummaryPresenter @Inject constructor(
finalGrade.isBlank() && predictedGrade.isBlank() && averages.singleOrNull { it.first == subject } == null finalGrade.isBlank() && predictedGrade.isBlank() && averages.singleOrNull { it.first == subject } == null
} }
} }
private fun formatAverage(average: Double, defaultValue: String = "-- --"): String {
return if (average == 0.0) defaultValue
else format(FRANCE, "%.2f", average)
}
} }

View File

@ -1,53 +0,0 @@
package io.github.wulkanowy.ui.modules.grade.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_grade_summary.*
class GradeSummaryScrollableHeader(private val finalAverage: String, private val calculatedAverage: String)
: AbstractFlexibleItem<GradeSummaryScrollableHeader.ViewHolder>() {
override fun getLayoutRes() = R.layout.scrollable_header_grade_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 {
gradeSummaryScrollableHeaderFinal.text = finalAverage
gradeSummaryScrollableHeaderCalculated.text = calculatedAverage
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as GradeSummaryScrollableHeader
if (calculatedAverage != other.calculatedAverage) return false
if (finalAverage != other.finalAverage) return false
return true
}
override fun hashCode(): Int {
var result = calculatedAverage.hashCode()
result = 31 * result + finalAverage.hashCode()
return result
}
class ViewHolder(view: View?, adapter: FlexibleAdapter<IFlexible<*>>?) : FlexibleViewHolder(view, adapter),
LayoutContainer {
override val containerView: View?
get() = contentView
}
}

View File

@ -1,5 +1,6 @@
package io.github.wulkanowy.ui.modules.grade.summary package io.github.wulkanowy.ui.modules.grade.summary
import io.github.wulkanowy.data.db.entities.GradeSummary
import io.github.wulkanowy.ui.base.BaseView import io.github.wulkanowy.ui.base.BaseView
interface GradeSummaryView : BaseView { interface GradeSummaryView : BaseView {
@ -12,7 +13,7 @@ interface GradeSummaryView : BaseView {
fun initView() fun initView()
fun updateData(data: List<GradeSummaryItem>, header: GradeSummaryScrollableHeader) fun updateData(data: List<GradeSummary>)
fun resetView() fun resetView()

View File

@ -0,0 +1,67 @@
package io.github.wulkanowy.ui.modules.homework
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import io.github.wulkanowy.data.db.entities.Homework
import io.github.wulkanowy.databinding.HeaderHomeworkBinding
import io.github.wulkanowy.databinding.ItemHomeworkBinding
import io.github.wulkanowy.utils.toFormattedString
import io.github.wulkanowy.utils.weekDayName
import org.threeten.bp.LocalDate
import javax.inject.Inject
class HomeworkAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
var items = emptyList<HomeworkItem<*>>()
var onClickListener: (Homework) -> 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) {
HomeworkItem.ViewType.HEADER.id -> HeaderViewHolder(HeaderHomeworkBinding.inflate(inflater, parent, false))
HomeworkItem.ViewType.ITEM.id -> ItemViewHolder(ItemHomeworkBinding.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 Homework)
}
}
@SuppressLint("DefaultLocale")
private fun bindHeaderViewHolder(binding: HeaderHomeworkBinding, date: LocalDate) {
with(binding) {
homeworkHeaderDay.text = date.weekDayName.capitalize()
homeworkHeaderDate.text = date.toFormattedString()
}
}
private fun bindItemViewHolder(binding: ItemHomeworkBinding, homework: Homework) {
with(binding) {
homeworkItemSubject.text = homework.subject
homeworkItemTeacher.text = homework.teacher
homeworkItemContent.text = homework.content
homeworkItemCheckImage.visibility = if (homework.isDone) View.VISIBLE else View.GONE
homeworkItemAttachmentImage.visibility = if (!homework.isDone && homework.attachments.isNotEmpty()) View.VISIBLE else View.GONE
root.setOnClickListener { onClickListener(homework) }
}
}
class HeaderViewHolder(val binding: HeaderHomeworkBinding) :
RecyclerView.ViewHolder(binding.root)
class ItemViewHolder(val binding: ItemHomeworkBinding) : RecyclerView.ViewHolder(binding.root)
}

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